13.4. РЕКУРСИВНЫЕ ФУНКЦИИ

В языках высокого уровня (и в C# в частности) допускается использование одноименных функций. В том случае, когда сигнатуры параметров таких функций различны, говорят о перегрузке функции. Этот термин общепринят, хотя, может быть, более правильным было бы говорить о перегрузке имени функции. Даже при одинаковых именах такие функции фактически являются разными подпрограммами. Вызовы одноименных функций предполагают обращение к разным подпрограммам.

Не запрещено вызывать из исполняющейся функции ту же самую функцию, т. е. использовать вызов функции с той же самой сигнатурой. В этом случае подпрограмма, не завершив свою работу, обращается к самой себе. Процесс обращения алгоритма к самому себе называется рекурсией. Соответственно, функции, вызывающие сами себя, называют рекурсивными.

На первый взгляд вызов функции самой себя до завершения работы является нелепостью. Функция вызывает себя, вновь вызванная функция вновь вызывает себя и т. д. Если этот процесс самовызовов ничем не ограничен, то неизбежно возникнет логическая ошибка вычислительного процесса. Ведь компьютер как исполнитель «понимает» каждую вызываемую функцию как новую и потому выделяет для него отдельные ресурсы компьютера (место в стеке или в куче, в частности), а они, эти самые ресурсы, конечны. Следовательно, возможно прекращение вычислительного процесса, сбой в решении задачи из-за перегрузки программного стека. Вместе с тем, использование рекурсии не запрещено. Более того, есть ряд задач, допускающих эффектное решение именно с помощью рекурсии, в то время как обычное решение, без рекурсии, может оказаться алгоритмически значительно более сложным. Именно соображения, связанные с поиском более простых и ясных решений, приводят к использованию рекурсивных функций.

НЕ нашли? Не то? Что вы ищете?

Каждый алгоритм, в том числе и рекурсивный, предназначен для решения какой-то частной задачи. Если алгоритм в процессе решения вновь вызывает сам себя, то это значит, что он делает попытку решить аналогичную задачу, но с иным набором исходных данных. Какие это могут быть данные? Если идет обработка последовательности чисел, то это может быть часть последовательности. Если идет обработка массива данных, то иным набором может быть тот же массив, но без первого или последнего элемента. В любом случае вызванная функция должна исполнить тот же алгоритм, но с меньшим набором данных. Другими словами, рекурсивная функция, вызванная повторно из самой себя, должна решить ту же задачу, но с данными меньшей размерности.

Рекурсивные функции используются при решении сложных задач, которые в действительности состоят из многократного решения одной и той же задачи с разными исходными данными. Решение каждой задачи называется шагом рекурсии. Каждый последующий шаг опирается на решение, полученное на предыдущем шаге. На каком-то из шагов рекурсии наступит момент, когда количество данных достигнет некоторой критической (начальной) величины. Точнее, это будут данные, для которых решение уже заранее известно – надо просто им воспользоваться. Значит, рекурсивная функция должна содержать хотя бы одну ветвь без самовызова, где задача решается для некоторой известной (критической, начальной) ситуации. А разобраться, какая именно ситуация имеет место (известная или требующая самовызова) можно с помощью операции сравнения, которая указывается в некотором блоке ветвления.

Для того чтобы построить рекурсивный алгоритм, необходимо несколько иначе посмотреть на условие задачи, как бы увидеть в задаче не одну, а множество повторяющихся однотипных задач. В соответствии с вышеприведенными рассуждениями, надо четко выделить:

    ситуацию или состояние данных, когда решение задачи неизвестно и, следовательно, надо вызвать функцию для решения задачи меньшей размерности; ситуацию или состояние данных, когда решение задачи известно или может быть легко найдено; условие, которое позволяет отделить одну ситуацию от другой.

Пример

Составить рекурсивную функцию подсчета факториала натурального числа.

Очевидно, что задача имеет простое итеративное решение. Формула для подсчета факториала известна:

N! = 1.N - 2)(N - 1)N,

причем по определению 0! = 1. Обычная функция может иметь следующий вид:

static int fact(int k)

{ int n, i;

if (k == 0) n = 1;

else for (n = 1, i = 1; i <= k; i++) n = n*i;

return n;

}

Попробуем увидеть в этой задаче не одну, а несколько задач. Ведь шаги цикла заставляют нас решать последовательно ряд задач: на первом шаге подсчитываем значение 1!, на втором – 2!, на третьем – 3!, причем каждый новый шаг опирается на решение, полученное на предыдущем шаге. При выполнении оператора n = n*i выполняется умножение значения i на только что полученное значение факториала для предыдущего числа. При этом мы опираемся на известное решение: 0! = 1. Если будет известно значение P(N - 1) для (N - 1)-го шага, то легко получить значение для N-го: P(N) = Р(n - 1).N.

Нам удалось представить задачу в виде нескольких задач. Начиная с задачи, которая имеет известное решение (0! = 1), каждая последующая задача имеет размерность на 1 большую, т. е. требует в качестве исходных данных на одно число больше. Очевидно, что если будет известно решение (N - 1)-ой задачи, то мы решим и N-ую, но для решения (N - 1)-ой нам придется воспользоваться тем же алгоритмом. Наконец, нет необходимости решать «ноль»-задачу – ее решение известно: 0! = 1.

Подведем итог. Мы имеем ситуацию с известным решением, когда N = 0 (факториал равен 1) и неизвестным решением, когда N > 0 – в этом случае сделаем шаг рекурсии. Мы можем различить эти ситуации по текущему значению N. Рекурсивная функция может иметь следующий вид:

static int fact(int k)

{ if (k==0) return 1;

return k*fact(k - 1);

}

Рекурсивный алгоритм является циклическим алгоритмом. Явно этот цикл не виден, так как он скрыт за рекурсивным вызовом. Любая задача, решаемая с помощью рекурсии, может быть решена с помощью обычного цикла, но описание такого решения может оказаться весьма громоздким и сложным.

А вот рекурсивные решения просты и достаточно изящны. Задачи некоторых классов решаются очень просто с помощью рекурсий. Единственная опасность, которая подстерегает любителя рекурсий – это конечность ресурсов компьютера, т. к. очень трудно заранее предвидеть, сколько именно их потребуется. Кроме того, при внешней простоте описания алгоритма конкретное исполнение его может оказаться более длительным по времени, чем исполнение соответствующего итеративного алгоритма – выделение ресурсов компьютера может занять больше времени, чем очередной шаг обычного циклического алгоритма.

14. КЛАССЫ В C#

14.1. КЛАССЫ: ЧЕМ ЭТО УДОБНО

Тот, кто изучил предыдущие разделы пособия, уже готов к уточнению ряда понятий, которые ранее было предложено принять на веру. К таким понятиям относятся «объект», «класс», «объектно-ориентированное программирование». Попробуем более четко их определить.

В разделе 12.2 мы определили понятие структуры. Структуры позволяют объединять в единое целое значения разных типов. Смысл такого объединения понятен – так удобно описывать реальные объекты, имеющие много разных характеристик. По сути переменная-структура и описывает некоторый объект. Вспомним, что переменная-структура является переменной значимого типа, т. е. ее значением является множество характеристик объекта.

В разделе 13.2 есть пример 4, который демонстрирует схему использования функции, обрабатывающей массив структур. Функция получает адрес массива через ссылку и выполняет добавление новой структуры на свободное место в массив. Можно представить себе и другие функции, работающие с массивом структур того же типа. Например, функцию сортировки массива по значению какого-либо поля или функцию поиска элемента, имеющего некоторое заданное значение какого-то поля – поиск студента по заданной фамилии. Это значит, что для работы с объектами-структурами может использоваться набор функций, выполняющих разные операции над заданным типом данных.

Отметим один любопытный нюанс такого использования структур и функций. При описании полей структуры мы указывали спецификатор public, гарантируя тем самым доступ к полю структуры из любой точки программы. Такая «вседозволенность» может быть источником ошибок программиста. Он может случайно, по ошибке изменить значение некоторого поля в таком месте программного текста, где не следовало бы этого делать.

В современных языках высокого уровня понятие структуры получило логическое развитие. Возникла мысль связать в единое целое структуру обрабатываемых данных и все функции, используемые при обработке этой структуры. Такой подход к разработке сложных программных систем получил название объектно-ориентированного стиля программирования или просто объектно-ориентированного программирования – сокращенно ООП. Средства ООП имеются во всех современных языках программирования высокого уровня и опираются на ряд понятий. Некоторые из понятий не являются чем-то новым, а представляют собой уже известные понятия, которые просто обозвали несколько иначе. Другие понятия являются действительно новыми, и их появление связано с развитием ООП.

Самым главным понятием ООП является объект. Оно как бы скопировано с реального мира, где каждый предмет, явление можно назвать объектом. В реальном мире каждый объект обладает некоторыми свойствами, признаками, которые, собственно, и позволяют отличить этот объект от других объектов, в том числе от объектов, ему подобных. Каждый объект имеет право на существование и существует, если им манипулируют по определенным правилам, не допуская его разрушения. Наконец, объект может выполнять некоторые действия или сам, по собственному желанию, или по принуждению, по требованию других объектов. Все перечисленное во многом присуще и объектам, используемым в ООП. Остальные понятия ООП являются производными от понятия «объект».

Реализация средств ООП основана на трёх основополагающих понятиях (концепциях). Эти понятия определяются следующими терминами: инкапсуляция, наследование, полиморфизм.

Термин инкапсуляция дословно переводится как «сокрытие». В ООП данные включаются в объект (агрегат данных) и связываются с некоторым программным кодом для их обработки. Тем самым данные разрешено изменять, но только так, как это прописано в названном программном коде. В сущности, это похоже на то, как в реальной жизни устроен реальный объект. Он также защищен оболочкой (заключен в капсулу), и его внутренний механизм может внешне управляться (изменяться) только с помощью конкретных, связанных с ним механизмов управления. Однако в отличие от реальных объектов, капсула в ООП может быть и «твердой», и «мягкой». Другими словами, в каких-то случаях манипулировать данными можно как угодно, в каких-то случаях есть «круг избранных», а в каких-то случаях данные могут быть вообще закрыты для изменений.

С точки зрения программиста каждый объект в программе является переменной некоторого типа. Этот тип определён на основе предопределенных или ранее определенных типов. Может показаться странным, что объект, который объединяет коды и данные, можно рассматривать как переменную. Однако применительно к ООП это именно так. Объект содержит только данные в виде структуры, а код обработки этих данных в готовой программе существует отдельно.

Каждый объект обязательно принадлежит какому-либо классу. Описание класса обязательно должно быть в программе. Имя класса как раз и является типом переменной-объекта. Если про переменную говорят, что она «является переменной такого-то типа», то про объект говорят, что он «принадлежит к такому-то классу». Таким образом, в ООП обычные переменные (предопределенных типов) принято называть переменными, а вот переменные типа «класс» уже называют объектами.

Понятием инкапсуляции мы и ограничимся в этом пособии. О наследовании и полиморфизме подробно рассказано в [1]. Здесь же мы обозначим чисто интуитивное представление об этих понятиях.

Термин наследование появился в развитие ООП. В реальной жизни под наследованием понимается проявление у наследников некоторых свойств, присущих родителям (случай наследования собственности здесь не вполне подходит, хотя при определенной доле фантазии и это можно использовать как аналогию). Надо, однако, четко понимать, что наследование, в общем-то, распространяется на классы, но не на объекты. Новый создаваемый класс (описание класса) может наследовать что-то от уже созданного ранее класса (созданного ранее описания), что проявляется в объектах нового класса. Объект нового класса имеет ряд свойств, присущих его классу, и может иметь свойства, унаследованные его классом у класса-родителя. В C# считается, что классы любого приложения (как встроенные, так и созданные программистом) образуют единую иерархию классов с точки зрения наследования. Самым первым в этой иерархии считается встроенный класс Object (о нем уже упоминалось ранее).

Термин полиморфизм также возник в развитие ООП. Внешне он проявляется в виде возможности использовать одно и то же имя алгоритма (процедуры или функции) для решения нескольких схожих, но технически различных задач. В реальной жизни полиморфизм может проявиться в использовании одного и того же названия для разных групп объектов. Например, слово «машина» имеет разный смысл для специалистов разных профессий. В то же время, целью полиморфизма в ООП является не просто использование одного имени, а использование общего имени для задания неких близких по смыслу действий, выполняемых в зависимости от типа применяемых данных. Примером проявления полиморфизма может быть перегрузка функций – правда, здесь, скорее, используется только одноименность.

В целом ООП построено на классах. Любую программную систему, выстроенную в объектном стиле, можно рассматривать как совокупность классов, возможно, объединенных в проекты, пространства имен, решения, как это делается при программировании на языке C#. У класса две различные роли. С одной стороны, класс – это модуль, архитектурная единица построения программной системы. Модульность построения – это вообще основное свойство программных систем. В ООП программная система, строящаяся по модульному принципу, состоит из классов, являющихся основным видом модуля. С другой стороны, класс – это тип данных. Точно так же, как типом данных является структура. Иногда говорят, что структура – это вырожденный класс.

Объектно-ориентированная разработка программной системы основана на стиле, называемом проектированием от данных. Проектирование программной системы сводится к поиску описаний данных (абстракций), подходящих для решения данной задачи. Каждая из таких абстракций реализуется в виде класса, которые и становятся модулями – архитектурными единицами построения нашей системы. В основе класса лежит абстрактный тип данных.

14.2. ОПИСАНИЕ И СОСТАВ КЛАССА

Теперь можно перейти к рассмотрению синтаксиса класса в C#. Для рассмотрения основ ООП нам будет достаточно упрощенного варианта синтаксиса – точный вариант рассмотрен в [1]. Итак, описание класса можно представить в следующем виде:

class имя_класса

{

Тело_класса

}

Слово class – служебное, смысл его понятен. Имя_класса – это произвольный идентификатор, не совпадающий с другими именами классов внутри данного пространства имен. Тело_класса в общем случае может содержать:

·  константы;

·  поля;

·  конструкторы;

·  методы-процедуры и функции;

·  методы-свойства;

·  методы-индексаторы;

·  методы-операции;

·  события;

·  делегаты;

·  деструкторы;

·  классы (структуры, интерфейсы, перечисления).

Не все компоненты перечисленного списка описаны в этом пособии. Рассмотрена только основа любого класса, т. е. поля, методы - процедуры и функции, конструкторы.

Поля класса синтаксически являются обычными переменными (объектами) языка. Их описание похоже на объявление переменных. Поля характеризуют состав объектов класса. Когда создается новый объект класса, то этот объект представляет собой набор полей класса. Два объекта одного класса имеют один и тот же набор полей, но разнятся значениями, хранимыми в этих полях. Каждое поле имеет модификатор доступа, принимающий одно из четырех значений: public, private, protected, internal (по умолчанию – private). Независимо от значения модификатора доступа, все поля доступны для всех методов данного класса. Если же поля доступны только для методов данного класса, то они имеют модификатор доступа private. Такие поля считаются закрытыми. Если некоторые поля должны быть доступны для методов любого другого класса, то эти поля следует снабдить модификатором public. Такие поля называются общедоступными или открытыми. Прочие модификаторы мы рассматривать сейчас не будем. Например:

string FIO; // закрытое поле

// доступно только методам того класса, где описано

public int priznak; // открытое общедоступное поле

Методы класса синтаксически являются обычными процедурами и функциями языка, поэтому их описание удовлетворяет обычным правилам объявления функций. Методы содержат описания действий, которые можно выполнять над объектами класса. Два объекта одного класса могут использовать один и тот же набор методов. Каждый метод, как и поле, имеет модификатор доступа, принимающий одно из ранее перечисленных значений (по умолчанию – private). Независимо от значения модификатора доступа, все методы доступны для вызова при выполнении метода внутри класса. Если метод имеет атрибут доступа private, возможно, опущенный, то тогда он доступен только для вызова внутри методов самого класса. Такие методы считаются закрытыми.

Понятно, что класс, у которого все методы закрыты, абсурден, поскольку никто не смог бы вызвать ни один из его методов. Как правило, у класса есть не только закрытые методы. Есть и открытые, задающие интерфейс класса – они снабжаются модификатором public. Интерфейс – это лицо класса, и именно он определяет, чем класс интересен при использовании, что он обеспечивает, какие сервисы предоставляет клиентам. Однако и закрытые методы составляют важную часть класса, позволяя клиентам не вникать во многие детали реализации. Эти методы клиентам класса недоступны, они о них могут ничего не знать, и, самое главное, изменения в закрытых методах никак не отражаются на клиентах класса при условии корректной работы открытых методов.

Когда создается новый объект, то в памяти создается структура данных с полями, определяемыми классом. Однако, не все поля отражаются в структуре объекта. У класса могут быть поля, связанные не с объектами, а с самим классом. Эти поля объявляются как статические с модификатором static. Статические поля доступны всем методам класса, потому что являются общими полями для всех объектов. Независимо от того, какой объект обрабатывается некоторым методом, используются одни и те же статические поля, позволяя методу использовать информацию, созданную другими объектами класса.

14.3. ДИНАМИЧЕСКИЕ И СТАТИЧЕСКИЕ МЕТОДЫ

Методы, описанные в классе, могут быть динамическими и статическими.

Динамический метод – это функция, предназначенная для обслуживания полей конкретного объекта – того, для которого она вызвана. Например, если есть объект класса Шар, то динамический метод Объем может использоваться для получения значения объема конкретного шара. Вызывается такой метод только в связи с объектом, т. е. функция не может быть вызвана просто по имени без указания объекта.

Статический метод отличается от динамического. Во-первых, описанием – перед типом функции указывается модификатор static. Такие функции вызываются просто по имени. Их работа не связана с обслуживанием полей конкретного объекта, а если и работает с полями каких-то объектов, то только тех, которые переданы в функцию через параметры. Именно такие функции мы рассматривали в разделе 13. Частым случаем применения статических методов является ситуация, когда класс предоставляет свои сервисы объектам других классов. Таковым является класс Math, рассмотренный ниже, который не имеет собственных полей – все его статические методы работают с объектами арифметических классов.

Таким образом, всякий вызов открытого динамического метода в объектных вычислениях имеет вид x. F(...), где x – это некоторый объект класса, а F(…) – некоторый метод. То же, если требуется обратиться к некоторому полю класса: x.pole. Правда, в этом случае поле должно иметь модификатор public. Если же необходимо вызвать статический метод или обратиться к статическому полю, то обращение будет иметь вид: имя_класса.F(…) или имя_класса.pole.

Неотъемлемым компонентом класса является особый динамический метод – конструктор, который представляет собой специальный метод класса, позволяющий создавать объекты класса. При этом может выполняться инициализация полей объекта теми значениями, которые нужны по замыслу программиста. Одна из синтаксических особенностей этого метода заключается в том, что его имя всегда совпадает с именем класса. Если программист не определяет конструктор класса, то к классу автоматически добавляется конструктор по умолчанию – конструктор без аргументов. Если программист сам создает один или несколько конструкторов, то автоматического добавления конструктора без аргументов не происходит.

Пример

class cilindr

{ double radius, vysota;

string name;

// конструктор объекта

public cilindr()

{radius = 0; vysota = 0; name = “пустой”;}

// еще один конструктор

public cilindr(double r, double v, string s)

{radius = r; vysota = v; name = s;}

}

class Program

{

static void Main()

{ cilindr A; // 1

cilindr B(10,10,”новый”); // 2

}

}

Рассмотрим, как создаются объекты. В операторе 1 для переменной A создается ссылка, пока висячая, со значением null. А вот оператор 2 будет отвергнут компилятором. Он не может дать переменной B нулевую ссылку, потому что программист претендует на конкретные значения полей, но для объекта еще нет места в памяти и некуда записать требуемые значения. В отличие от структур все переменные-объекты класса – это переменные ссылочного типа. Поэтому правильно записать так:

{ cilindr A;

cilindr B;

B = new cilindr(10, 10,”новый”);

}

Теперь для переменной B в динамической памяти выделено место операцией new. Адрес этого места помещается в B, а в полях объекта в динамической памяти появляются указанные значения – работает второй конструктор класса, имеющий три параметра. Тем не менее, компиляция такой программы снова даст ошибку – теперь уже на переменную A. Переменная A объявлена, но не используется. Поэтому правильно, например, так:

{ cilindr A;

cilindr B;

B = new cilindr(10, 10,”новый”);

A = new cilindr();

}

В этом случае переменная A формируется как объект в динамической памяти с помощью первого конструктора без параметров. Поля объекта A получают «нулевые» значения, а переменная A заполняется адресом этого объекта.

Зачем классу нужно несколько конструкторов? Дело в том, что в зависимости от контекста и создаваемого объекта может требоваться различная инициализация его полей. Перегрузка конструкторов (использование нескольких конструкторов с разными сигнатурами) и обеспечивает решение этой задачи. Любопытно, что конструктор может быть объявлен с атрибутом private. Понятно, что в этом случае внешний пользователь не может воспользоваться им для создания объектов. Но это могут делать методы класса, создавая объекты для собственных нужд со специальной инициализацией.

14.4. КЛАССЫ Math И Random

В качестве примеров встроенных классов рассмотрим классы Math и Random.

Класс Math содержит стандартные математические функции, без которых трудно обойтись при построении многих вычисляемых выражений. В предыдущем разделе было отмечено, что одним из вариантов применения статических методов является ситуация, когда класс предоставляет свои сервисы объектам других классов. Как раз таким и является класс Math. Все его статические методы работают с объектами арифметических классов.

Этот класс содержит два статических поля: E (число е) и PI (число ПИ), а также более 20 статических методов. В списке методов имеются методы, вычисляющие тригонометрические функции (Sin, Cos, Tan), обратные тригонометрические функции (ASin, ACos, ATan), экспоненту и логарифмические функции (Exp, Log, Log10), модуль числа (Abs), квадратный корень (Sqrt), степень числа (Pow), а также ряд других. Следующий пример, не требующий пояснения, демонстрирует использование статических функций класса для вычисления площади сектора.

Console. Write("Радиус R = ");

temp = Console. ReadLine();

Rad = Convert. ToDouble(temp);

Console. Write(" Угол сектора = ");

temp = Console. ReadLine();

Alpha = Convert. ToDouble(temp);

s = Math. PI*Math. Pow(Rad,2.0)*Alpha/360.0);

Console. WriteLine("Площадь сектора = {0}”, s);

Класс Random содержит все необходимые средства для организации различных генераторов случайных чисел. Особенность этого класса заключается в том, что его функциональные возможности проявляются в динамических методах. Класс Random имеет конструктор класса: для того чтобы вызывать методы класса, нужно вначале создавать экземпляр (объект) класса. Созданный объект обеспечивает получение псевдослучайных (почти случайных) чисел. Этим Random отличается от класса Math, у которого все поля и методы – статические, что позволяет обойтись без создания экземпляров класса Math.

Как и любой другой класс, класс Random является наследником класса Object, а, следовательно, имеет в своем составе и методы родителя. Мы рассмотрим часть оригинальных методов класса Random, используемых для генерирования последовательностей случайных чисел.

Начнем рассмотрение с конструктора класса. Конструктор обеспечивает создание объектов (экземпляров) класса. Он имеет две реализации. Одна из них позволяет генерировать неповторяющиеся при каждом запуске серии случайных чисел. Начальный элемент такой серии строится на основе текущей даты и времени, что гарантирует уникальность серии, так как дата и время различны при разных запусках программы. Этот конструктор вызывается без параметров. Он описан в классе как Random(). Другой конструктор с параметром целого типа имеет сигнатуру Random (int) и обеспечивает важную возможность генерирования повторяющейся серии случайных чисел. Параметр конструктора используется для построения начального элемента серии, поэтому при задании одного и того же значения параметра серия будет повторяться.

Перегруженный метод Next() при каждом вызове возвращает положительное целое, равномерно распределенное в некотором диапазоне. Диапазон задается параметрами метода. Три реализации метода отличаются набором параметров:

Next () – метод без параметров выдает целые положительные числа во всем положительном диапазоне типа int;

Next (max) – выдает целые положительные числа в диапазоне [0, max];

Next (min, max) – выдает целые положительные числа в диапазоне [min, max].

Метод NextDouble () имеет одну реализацию. При каждом вызове этого метода выдается новое случайное число из интервала (0, 1).

Еще один полезный метод класса Random позволяет при одном обращении получать целую серию случайных чисел. Метод имеет параметр – массив, который и будет заполнен случайными числами. Метод описан как NextBytes (buffer). Так как параметр buffer представляет массив байтов, то, естественно, генерированные случайные числа находятся в диапазоне [0, 255].

Рассмотрим пример работы со случайными числами.

Random realRnd = new Random( );

Random repeatRnd = new Random(77);

// случайные числа в диапазоне [0,1]

Console. WriteLine("случайные вещественные числа от 0 до 1)");

for(int i = 1; i <= 5; i++)

{

Console. WriteLine(realRnd. NextDouble( ));

}

// случайные числа в диапазоне[min, max]

int min = – 100, max = – 10;

for(int i = 1; i <= 5; i++)

{

Console. WriteLine(realRnd. Next(min, max));

}

// случайный массив байтов в диапазоне от 0 до 255

byte [ ] bar = new byte [10];

repeatRnd. NextBytes(bar);

for (int i = 0; i < 10; i++)

{

Console. WriteLine (bar[i]);

}

Краткий комментарий к тексту программы. Вначале создаются два объекта класса Random. У этих объектов разные конструкторы. Объект с именем realRnd позволяет генерировать неповторяющиеся серии случайных чисел. Объект repeatRnd дает возможность повторить при необходимости серию. Метод NextDouble создает серию случайных чисел в диапазоне [0, 1]. Вызываемый в цикле метод Next с двумя параметрами создает серию случайных отрицательных целых, равномерно распределенных в диапазоне [-100, -10]. Метод NextBytes объекта repeatRnd позволяет получить при одном вызове массив случайных чисел из диапазона [0, 255].

15. КЛАССЫ ВВОДА И ВЫВОДА

15.1. ПОНЯТИЕ ПОТОКА

В данном разделе будут рассмотрены встроенные классы, без использования которых невозможно работать с данными на внешних носителях, например, на дисковых накопителях. Данные на дисках размещаются в файлах. Файл – это конечное количество последовательно размещенных байтов, похожее на массив. Не случайно файл иногда называют виртуальным массивом. Передача данных из файла в память компьютера называют чтением (вводом) данных, а передачу из памяти в файл – записью (выводом) данных. Такие устройства, как клавиатура, дисплей, принтер, могут рассматриваться как частные случаи файлов.

В языке C# ввод и вывод обеспечиваются подсистемой классов встроенной библиотеки. Так как язык является наследником языка C, то он сохранил механизм ввода-вывода, свойственный своему предку. Как и в C, обмен данными реализуется с помощью потоков.

Поток – это последовательность байтов. В сущности, это довольно абстрактное понятие. Последовательность байтов может перемещаться от любого устройства к любому другому устройству. Важно только, чтобы поток был «привязан» к соответствующему устройству. При работе с потоком часто используется вспомогательный участок основной памяти – буфер потока. Использование буфера уменьшает количество обменов между памятью и внешним устройством. С внешнего устройства данные считываются (вводятся) в память не по одному байту, а блоком, который размещается в буфере. Из буфера данные могут выбираться программой побайтно или группами байтов. При записи данных на внешнее устройство (вывод данных) данные сначала накапливаются в буфере (по одному байту или группами), а затем весь буфер выводится на внешнее устройство. Это значительно увеличивает скорость обмена данными с внешним устройством: одно дело обмениваться по одному байту, а другое – целыми блоками байтов.

Потоки делятся на три типа:

·  входные – это потоки, вводящие данные в оперативную память;

·  выходные – это потоки, выводящие данные из памяти;

·  двунаправленные – это потоки, позволяющие направлять данные и в память, и из нее.

Все потоки последовательные, т. е. в каждый момент времени для потока определены позиция записи или чтения. После обмена данными (ввод из потока или вывод в него) эта позиция смещается на длину введенной или выведенной порции данных.

15.2. КЛАССЫ ПРОСТРАНСТВА ИМЕН System. IO

Для обслуживания различных потоков в C# имеется набор классов. Этот набор включен в пространство имен System.IO. К числу наиболее часто используемых классов относятся:

Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 5 6