Глава 4

 Операторы

 

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

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

Структурный подход, сформировавшийся в 60-70-х годах прошлого столетия, позволил довольно успешно создавать достаточно крупные проекты, но слож­ность программного обеспечения продолжала возрастать, и требовались все более развитые средства ее преодоления. Идеи структурного программирования по­лучили свое дальнейшее развитие в объектно-ориентированном программирова­нии — технологии, позволяющей достичь простоты структуры и управляемости очень больших программных систем.

Несмотря на то что язык С# реализует объектно-ориентированную парадигму, принципы структурного программирования лежат в основе кодирования каждо­го метода, каждого фрагмента алгоритма.

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

 

Выражения, блоки и пустые операторы

 

Любое выражение, завершающееся точкой с запятой, рассматривается как опера­тор, выполнение которого заключается в вычислении выражения. Частным слу­чаем выражения является пустой оператор; (он используется, когда по синтак­сису оператор требуется, а по смыслу — нет). Примеры:

 

 

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

 

Операторы ветвления

 

Операторы ветвления if и switch применяются для того чтобы в зависимости от конкретных значений исходных данных обеспечить выполнение разных после­довательностей операторов. Оператор if обеспечивает передачу управления на одну, из двух ветвей вычислений, а оператор switch — на одну из произвольного числа ветвей.

 

 

Условный оператор if

 

Условный оператор if используется для разветвления процесса вычислений на два направления. Структурная схема оператора приведена на рис. 4.1.

 

 

Формат оператора:

 

if ( логическое_выражение ) оператор_1; [ else оператор_2; ]

 

Сначала вычисляется логическое выражение. Если оно имеет значение true, вы­полняется первый оператор, иначе — второй. После этого управление передается на оператор, следующий за условным. Ветвь else может отсутствовать.                 

ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------

Операторы, входящие в условный, не должны иметь метку и не могут быть описа­ниями.

_______________________________________________________________

 

Если в какой-либо ветви требуется выполнить несколько операторов, их необхо­димо заключить в блок, иначе компилятор не сможет понять, где заканчивается ветвление. Блок может содержать любые операторы, в том числе описания и другие условные операторы (но не может состоять из одних описаний). Необходимо учи­тывать, что переменная, описанная в блоке, вне блока не существует.

Примеры условных операторов:   

 

 

В примере 1 отсутствует ветвь else. Подобная конструкция реализует пропуск оператора, поскольку присваивание либо выполняется, либо пропускается в за­висимости от выполнения условия.

Если требуется проверить несколько условий, их объединяют знаками логиче­ских условных операций. Например, выражение в примере 2 будет истинно в том случае, если выполнится одновременно условие а < b и одно из условий в скоб­ках. Если опустить внутренние скобки, будет выполнено сначала логическое И, а потом — ИЛИ.

Оператор в примере 3 вычисляет наибольшее значение из трех переменных. Об­ратите внимание на то, что компилятор относит часть else к ближайшему ключе­вому слову if.

Конструкции, подобные оператору в примере 4 (вычисляется наибольшее значе­ние из двух переменных), проще и нагляднее записывать в виде условной опера­ции, в данном случае следующей:

 

max = b > а ? b : а;

 

ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------

Распространенная ошибка начинающих — неверная запись проверки на принадлеж­ность диапазону. Например, чтобы проверить условие 0 < х < 1, нельзя записать его в условном операторе непосредственно, так как каждая операция отношения долж­на иметь два операнда. Правильный способ записи: if (0 < х && х < 1)....

---------------------------------------------------------------------------------------------------------------------

В качестве примера подсчитаем количество очков после выстрела по мишени, изображенной на рис. 4.2.

 

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

 

 

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

if:

 

double temp = x*x + y*y;

int kol = 0;

if ( temp < 4 ) kol = 1:

if ( temp < 1 ) kol = 2;

Console.WriteLine( "Результат = {0} очков", kol  );

 

Обратите внимание на то, что в первом варианте программы второй оператор if выполняется только в случае невыполнения условия в первом условном опера­торе, а во втором варианте оба условных оператора выполняются последователь­но, один за другим.

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

 

float a, b; ...

if   ( а == b ) ...                                        //не рекомендуется!

if   ( Math.Abs(a - b) < le-6 ) _                // надежно!

 

Значение величины, с которой сравнивается модель разности, следует выбирать в зависимости от решаемой задачи и точности участвующих в выражении пере­менных. Снизу эта величина ограничена определенной в классах Single и Double константой Epsilon (это минимально возможное значение переменной такое,

что 1.0 + Epsilon != 1.0).

 

Оператор выбора switch

 

Оператор switch (переключатель) предназначен для разветвления процесса вы­числений на несколько направлений. Структурная схема оператора приведена на рис. 4.3.

 

Формат оператора:

switch ( выражение ) {

case константное_выражение_1:  [ список_операторов_1 ] case константное_выражение_2:           [ список_операторов_2 ]

case константное_выражение_n: [ список_операторов_n ] [ default: операторы ]

 

}

 

Выполнение оператора начинается с вычисления выражения. Тип выражения чаще всего целочисленный (включая char) или строковый. Затем управление передается первому оператору из списка, помеченному константным выражени­ем, значение которого совпало с вычисленным.

Все константные выражения должны быть неявно приводимы к типу выражения в скобках. Если совпадения не произошло, выполняются операторы, расположен­ные после слова default (а при его отсутствии управление передается следующему за switch оператору).                                                                                       

Каждая ветвь переключателя должна заканчиваться явным оператором перехо­да, а именно оператором break, goto или return:

□   оператор break выполняет выход из самого внутреннего из объемлющих его операторов switch, for, while и do (см. раздел «Оператор break», с. 84);

□   оператор goto выполняет переход на указанную после него метку, обычно это метка case одной из нижележащих ветвей оператора switch (см. раздел «Опeратор goto», с. 83);

□   оператор return выполняет выход из функции, в теле которой он записан (см. раздел «Оператор return», с. 87).

Оператор goto обычно используют для последовательного выполнения несколь­ких ветвей переключателя, однако поскольку это нарушает читабельность про­граммы, такого решения следует избегать.

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

 

 

 

 

СОВЕТ-----------------------------------------------------------------------------------------------------

Хотя наличие ветви default и не обязательно, рекомендуется всегда обрабатывать случай, когда значение выражения не совпадает ни с одной из констант. Это облег­чает поиск ошибок при отладке программы.

---------------------------------------------------------------------------------------------------------------------

Оператор switch предпочтительнее оператора if в тех случаях, когда в программе требуется разветвить вычисления на количество направлений большее двух и вы­ражение, по значению которого производится переход на ту или иную ветвь, не является вещественным. Часто это справедливо даже для двух ветвей, поскольку повышает наглядность программы.

 

Операторы цикла

 

Операторы цикла используются для вычислений, повторяющихся многократно. В С# имеется четыре вида циклов: цикл с предусловием while, цикл с постусло­вием repeat, цикл с параметром for и цикл перебора foreach. Каждый, из них со­стоит из определенной последовательности операторов.

Блок, ради выполнения которого и организуется цикл, называется телом цикла. Остальные операторы служат для управления процессом повторения вычис­лений: это начальные установки, проверка условия продолжения цикла и моди­фикация параметра цикла (рис. 4.4). Один проход цикла называется итерацией.

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

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

 

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

Цикл завершается, если условие его продолжения не выполняется. Возможно принудительное завершение как текущей итерации, так и цикла в целом. Для этого служат операторы break, continue, return и goto (см. раздел «Операторы передачи управления», с. 83). Передавать управление извне внутрь цикла запре­щается (при этом возникает ошибка компиляции).

 

Цикл с предусловием while

 

Формат оператора прост:

while ( выражение ) оператор

 

Выражение должно быть логического типа. Например, это может быть операция от­ношения или просто логическая переменная. Если результат вычисления выраже­ния равен true, выполняется простой или составной оператор (блок). Эти действия повторяются до того момента, пока результатом выражения не станет значение false. После окончания цикла управление передается на следующий за ним оператор.

Выражение вычисляется перед каждой итерацией цикла. Если при первой про­верке выражение равно false, цикл не выполнится ни разу.

ВНИМАНИЕ--------------------------------------------------------------------------------------------------

Если в теле цикла необходимо выполнить более одного оператора, необходимо за­ключить их в блок с помощью фигурных скобок.

---------------------------------------------------------------------------------------------------------------------

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

ледую­щей функции:

 

Назовем начальное значение аргумента Хn, конечное значение аргумента — Хk, шаг изменения аргумента — dX и параметр t. Все величины вещественные. Про­грамма должна выводить таблицу, состоящую из двух столбцов: значений аргу­мента и соответствующих им значений функции.

Опишем алгоритм в словесной форме:

1.   Взять первое значение аргумента.

2.  Определить, какому из интервалов оно принадлежит, и вычислить значение функции по соответствующей формуле.

3.  Вывести строку таблицы.

4.   Перейти к следующему значению аргумента.

5.  Если оно не превышает конечное значение, повторить шаги 2-4, иначе закончить.

Шаги 2-4 повторяются многократно, поэтому для их выполнения надо органи­зовать цикл. Текст программы приведен в листинге 4.3. Строки программы по­мечены соответствующими номерами шагов алгоритма. Обратите внимание на то, что условие продолжения цикла записано в его заголовке и проверяется до входа в цикл. Таким образом, если задать конечное значение аргумента, меньшее начального, даже при отрицательном шаге цикл не будет выполнен ни разу.

 

 

ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------

При выводе использованы дополнительные поля спецификаций формата, опреде­ляющие желаемую ширину поля вывода под переменную (в данном случае — 6 по­зиций). В результате колонки при выводе таблицы получаются ровными. Описание спецификаций формата приведено в приложении.

-------------------------------------------------------------------------------------------------------------------

Параметром этого цикла, то есть переменной, управляющей его выполнением, является х. Блок модификации параметра цикла представлен оператором, вы­полняющимся на шаге 4. Для перехода к следующему значению аргумента теку­щее значение наращивается на величину шага и заносится в ту же переменную. Начинающие часто забывают про модификацию параметра, в результате програм­ма «зацикливается». Если с вами произошла такая неприятность, попробуйте для завершения программы нажать клавиши Ctrl+Break, а впредь перед запуском программы проверяйте:

□   присвоено ли параметру цикла верное начальное значение;

□   изменяется ли параметр цикла на каждой итерации цикла;

□   верно ли записано условие продолжения цикла.

Распространенным приемом программирования является организация бесконеч­ного цикла с заголовком while (true) и принудительным выходом из тела цикла по выполнению какого-либо условия с помощью операторов передачи управле­ния. В листинге 4.4 приведен пример использования бесконечного цикла для ор­ганизации меню программы.

 

 

 

Цикл с постусловием do

 

 

Цикл с постусловием реализует структурную схему, приведенную на рис. 4.4, 6, и имеет вид:

do оператор while выражение;

 

Сначала выполняется простой или составной оператор, образующий тело цикла, а затем вычисляется выражение (оно должно иметь тип bool). Если выражение истинно, тело цикла выполняется еще раз и проверка повторяется. Цикл завер­шается, когда выражение станет равным false или в теле цикла будет выполнен какой-либо оператор передачи управления.

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

Пример программы, выполняющей проверку ввода, приведен в листинге 4.5

 

 

 

Рассмотрим еще один пример применения цикла с постусловием — программу, определяющую корень уравнения cos(x) = x методом деления пополам с точно­стью 0,0001.

Исходные данные для этой задачи — точность, результат — число, представляю­щее собой корень уравнения. Оба значения имеют вещественный тип.

Суть метода деления пополам очень проста. Задается интервал, в котором есть ровно один корень (следовательно, на концах этого интервала функция имеет значения разных знаков). Вычисляется значение функции в середине этого ин­тервала. Если оно того же знака, что и значение на левом конце интервала, зна­чит, корень находится в правой половине интервала, иначе — в левой. Процесс повторяется для найденной половины интервала до тех пор, пока его длина не станет меньше заданной точности.

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

 

.

 

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

 

Цикл с параметром for

 

Цикл с параметром имеет следующий формат:

 

for ( инициализация; выражение; модификации ) оператор;

 

Инициализация служит для объявления величин, используемых в цикле, и при­своения им начальных значений. В этой части можно записать несколько опера­торов, разделенных запятой, например:

 

for ( int i = 0, j = 20;   ...

int k, m;

for ( k = 1, m = 0;   ...

 

Областью действия переменных, объявленных в части инициализации цикла, яв­ляется цикл. Инициализация выполняется один раз в начале исполнения цикла.

 Выражение типа bool определяет условие выполнения цикла: если его результат равен true, цикл выполняется. Цикл с параметром реализован как цикл с преду­словием.

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

 

for ( int i = 0. j = 20;  i < 5 && j > 10; i++, j-- ) ...

 

Простой или составной оператор представляет собой тело цикла. Любая из час­тей оператора for может быть опущена (но точки с запятой надо оставить на сво­их местах!). Для примера вычислим сумму чисел от 1 до 100:

 

int s = 0;

for ( int i = 1;  i <= 100;  i++ ) s += i;

 

В листинге 4.7 приведена программа, выводящая таблицу значений функции из листинга 4.3.

 

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

СОВЕТ ---------------------------------------------------------------------------------------------------------

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

--------------------------------------------------------------------------------------------------------------------

Любой цикл while может быть приведен к эквивалентному ему циклу for и на­оборот. Например, два следующих цикла эквивалентны:

 

 

Цикл перебора foreach

 

Цикл foreach используется для просмотра всех объектов из некоторой группы данных, например массива, списка или другого контейнера. Он будет рассмот­рен, когда у нас появится в нем необходимость, а именно в разделе «Оператор foreach» (см. с. 136).

 

Рекомендации по выбору оператора цикла

 

Операторы цикла взаимозаменяемы, но можно привести некоторые рекоменда­ции по выбору наилучшего в каждом конкретном случае.

Оператор do while обычно используют, когда цикл требуется обязательно выпол­нить хотя бы раз, например, если в цикле производится ввод данных.

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

Оператор foreach применяют для просмотра элементов различных коллекций объектов.

Оператор for предпочтительнее в большинстве остальных случаев. Однозначно — для организации циклов со счетчиками, то есть с целочисленными переменны­ми, которые изменяют свое значение при каждом проходе цикла регулярным об­разом (например, увеличиваются на 1).

Начинающие часто делают ошибки при записи циклов. Чтобы избежать этих ошибок, рекомендуется:

□   проверить, всем ли переменным, встречающимся в правой части операторов присваивания в теле цикла, присвоены до этого правильные начальные значе­ния (а также возможно ли выполнение других операторов);

□  проверить, изменяется ли в цикле хотя бы одна переменная, входящая в усло­вие выхода из цикла;                                                                                  

□   предусмотреть аварийный выход из цикла по достижении некоторого коли­чества итераций (пример приведен в следующем разделе);

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

 

Операторы передачи управления

 

В С# есть пять операторов, изменяющих естественный порядок выполнения вы­числений:

□   оператор безусловного перехода goto;

□   оператор выхода из цикла break

□  оператор перехода к следующей итерации; цикла continue;

□   оператор возврата из функции return;

□   оператор генерации исключения throw.

Эти операторы могут передать управление в пределах блока, в котором они ис­пользованы, и за его пределы. Передавать управление внутрь другого блока за­прещается.

Первые четыре оператора рассматриваются в этом разделе, а оператор throw — далее в этой главе на с. 93.

 

Оператор goto

 

Оператор безусловного перехода goto используется в одной из трех форм:           

 

goto метка;                                                                                                                                    goto case константное_выражение;

goto default;                                                                                                                                

 

В теле той же функции должна присутствовать ровно одна конструкция вида  

 

метка: оператор;

 

Оператор goto метка передаёт управление на помеченный оператор. Метка — это обычный идентификатор, областью видимости которого является функция, в теле которой он задан. Метка должна находиться в той же области видимости, что и оператор перехода. Использование этой формы оператора безусловного пере­хода оправдано в двух случаях:

□   принудительный выход вниз по тексту программы из нескольких вложенных циклов или переключателей;           

□  переход из нескольких точек функции вниз по тексту в одну точку (например, если перед выходом из функции необходимо всегда выполнять какие-либо действия).

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

Вторая и третья формы оператора goto используются в теле оператора выбора switch. Оператор goto case константное_выражение передает управление на соответ­ствующую константному выражению ветвь, а оператор goto default — на ветвь default. Надо отметить, что реализация оператора выбора в С# на редкость не­удачна, и наличие в нем оператора безусловного перехода затрудняет понимание программы, поэтому лучше обходиться без него.

 

Оператор break

 

Оператор break используется внутри операторов цикла или выбора для перехода в точку программы, находящуюся непосредственно за оператором, внутри кото­рого находится оператор break.

Для примера рассмотрим программу вычисления значения функции ch x (гипер­болический косинус) с точностью е = 10-6с помощью бесконечного ряда Тейлора по формуле

 

 

Этот ряд сходится при | х‌ |  < ∞. Для достижения заданной точности требуется суммировать члены ряда, абсолютная величина которых больше ε. Для сходяще­гося ряда модуль члена ряда Сп при увеличении п стремится к нулю. При некотором п неравенство

| Сп |  ≥ ε перестает выполняться и вычисления прекращаются.

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

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

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

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

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

 

 

 

В листинге 4.8 приведен текст программы с комментариями.

 

 

 

Получение суммы бесконечного ряда — пример вычислений, которые принципи­ально невозможно выполнить точно. В данном случае мы задавали желаемую погрешность вычислений с помощью значения ε. Это значение не может быть меньше, чем самое малое число, представимое с помощью переменной типа double но при задании такого значения точность результата фактически будет гораздо ниже из-за погрешностей, возникающих при вычислениях. Они связаны с конеч­ностью разрядной сетки.

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

□  погрешность постановки задачи (возникает при упрощении задачи);

□   начальная погрешность (точность представления исходных данных);

□   погрешность метода (при использовании приближенных методов решения задачи);

□   погрешности округления и вычисления (поскольку величины хранятся в ог­раниченном количестве разрядов).

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

 

Оператор continue

 

Оператор перехода к следующей итерации текущего цикла continue пропускает все операторы, оставшиеся до конца тела цикла, и передает управление на нача­ло следующей итерации.

Перепишем основной цикл листинга 4.8 с применением оператора continue:

 

 

Оператор return

 

Оператор возврата из функции return завершает выполнение функции и переда­ет управление в точку ее вызова. Синтаксис оператора:

 

return [ выражение ];

 

Тип выражения должен иметь неявное преобразование к типу функции. Если тип возвращаемого функцией значения описан как void, выражение должно от­сутствовать.

 

Базовые конструкции структурного программирования

 

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

Иными словами, современные критерии качества программы — это, прежде все­го, надежность, а также возможность точно планировать производство програм­мы и ее сопровождение. Для достижения этих целей программа должна иметь простую структуру, быть читабельной и легко модифицируемой. Технология структурного программирования позволяет создавать как раз такие программы небольшого и среднего объема. Для разработки более сложных комплексов тре­буется применять объектно-ориентированное программирование.

В С# идеи структурного программирования используются на самом низком уровне — при написании методов объектов. Доказано, что любой алгоритм мож­но реализовать только из трех структур, называемых базовыми конструкциями структурного программирования, — это следование, ветвление и цикл.

Следованием называется конструкция, реализующая последовательное выполне­ние двух или более операторов (простых или составных). Ветвление задает выполнение либо одного, либо другого оператора в зависимости от выполнения какого-либо условия.

Цикл реализует многократное выполнение оператора. Базо­вые конструкции приведены на рис. 4.5.

 

 

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

 

 

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

В большинстве языков высокого уровня существует несколько реализаций ба­зовых конструкций; в С# есть четыре вида циклов и два вида ветвлений (на два и на произвольное количество направлений). Они введены для удобства програм­мирования, и в каждом случае надо выбирать наиболее подходящие средства. Главное, о чем нужно помнить даже при написании самых простых программ, — они должны состоять из четкой последовательности блоков строго определен­ной структуры. «Кто ясно мыслит, тот ясно излагает» — практика давно показа­ла, что программы в стиле «поток сознания» нежизнеспособны, не говоря о том, что они просто некрасивы.

 

Обработка исключительных ситуаций

 

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

Исключительная ситуация, или исключение, — это возникновение аварийного со­бытия, которое может порождаться некорректным использованием аппаратуры или неправильной работой программы, например делением на ноль или пере­полнением. Обычно эти события приводят к завершению программы с систем­ным сообщением об ошибке. С# дает программисту возможность восстановить работоспособность программы и продолжить ее выполнение.

Исключения С# не поддерживают обработку асинхронных событий, таких как ошибки оборудования или прерывания, например нажатие клавиш Ctrl+C. Меха­низм исключений предназначен только для событий, которые могут произойти в результате работы самой программы и указываются явным образом. Исключе­ния возникают тогда, когда некоторая часть программы не смогла сделать то, что от нее требовалось. При этом другая часть программы может попытаться сделать что-нибудь иное.

Исключения позволяют логически разделить вычислительный процесс на две час­ти — обнаружение аварийной ситуации и ее обработка. Это важно не только для лучшей структуризации программы. Главное то, что функция, обнаружившая ошибку, может не знать, что предпринимать для ее исправления, а использую­щий эту функцию код может знать, что делать, но не уметь определить место возникновения. Это особенно актуально при использовании библиотечных функ­ций и программ, состоящих из многих модулей.

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

ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------

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

--------------------------------------------------------------------------------------------------------------------

Исключения генерирует либо среда выполнения, либо программист с помощью оператора throw. В табл. 4.1 приведены наиболее часто используемые стандарт­ные исключения, генерируемые средой. Они определены в пространстве имен System. Все они являются потомками класса Exception, а точнее, потомками его потомка System Exception.

Исключения обнаруживаются и обрабатываются в операторе try.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


Оператор try

 

Оператор try содержит три части:

□  контролируемый блок — составной оператор, предваряемый ключевым сло­вом try. В контролируемый блок включаются потенциально опасные операто­ры программы. Все функции, прямо или косвенно вызываемые из блока, так­же считаются ему принадлежащими;

□  один или несколько обработчиков исключений — блоков catch, в которых опи­сывается, как обрабатываются ошибки различных типов;

□  блок завершения finally выполняется независимо от того, возникла ошибка в контролируемом блоке или нет.

Синтаксис оператора try:

 

try блок [ блоки catch ] [ блок finally ]

 

Отсутствовать могут либо блоки catch, либо блок finally, но не оба одновре­менно.

Рассмотрим, каким образом реализуется обработка исключительных ситуаций.

1.   Обработка исключения начинается с появления ошибки. Функция или опе­рация, в которой возникла ошибка, генерирует исключение. Как правило, ис­ключение генерируется не непосредственно в блоке try, а в функциях, прямо или косвенно в него вложенных.

2.   Выполнение текущего блока прекращается, отыскивается соответствующий обработчик исключения, и ему передается управление.

3.   Выполняется блок finally, если он присутствует (этот блок выполняется и в том случае, если ошибка не возникла).

4. Если обработчик не найден, вызывается стандартный обработчик исключе­ния. Его действия зависят от конфигурации среды. Обычно он выводит на эк­ран окно с информацией об исключении и завершает текущий процесс.

ПРИМЕЧАНИЕ----------------------------------------------------------------------------------~----

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

---------------------------------------------------------------------------------------------------------------

Обработчики исключений должны располагаться непосредственно за блоком try. Они начинаются с ключевого слова catch, за которым в скобках следует тип об­рабатываемого исключения. Можно записать один или несколько обработчиков в соответствии с типами обрабатываемых исключений. Блоки catch просматри­ваются в том порядке, в котором они записаны, пока не будет найден соответствующий типу выброшенного исключения.      

Синтаксис обработчиков напоминает определение функции с одним парамет­ром — типом исключения. Существуют три формы записи:

 

catch( тип имя )      { ... /* тело обработчика */ }

catch( тип )             {.../* тело обработчика */ }

 catch                      { ... /* тело обработчика */ }

 

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

Вторая форма не предполагает использования информации об исключении, иг­рает роль только его тип.

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

 

 

Если исключение в контролируемом блоке не возникло, все обработчики про­пускаются.

В любом случае, произошло исключение или нет, управление передается в блок завершения finally (если он существует), а затем — первому оператору, находяще­муся непосредственно за оператором try. В завершающем блоке обычно записы­ваются операторы, которые необходимо выполнить независимо от того, возник­ло исключение или нет, например, закрытие файлов, с которыми выполнялась работа в контролируемом блоке, или вывод информации.

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

ПРИМЕЧАНИЕ ----------------------------------------------------------------------------------------------Исключение, связанное с делением на ноль, для вещественных значений возник­нуть не может, поэтому не проверяется. При делении на ноль будет выдан резуль­тат «бесконечность».

 

 

Операторы try могут многократно вкладываться друг в друга. Исключение, ко­торое возникло во внутреннем блоке try и не было перехвачено соответствую­щим блоком catch, передается на верхний уровень, где продолжается поиск под­ходящего обработчика. Этот процесс называется распространением исключения. Распространение исключений предоставляет программисту интересные возмож­ности. Например, если на внутреннем уровне недостаточно информации для того, чтобы провести полную обработку ошибки, можно выполнить частичную обра­ботку и сгенерировать исключение повторно, чтобы оно было обработано на верх­нем уровне. Генерация исключения выполняется с помощью оператора throw.

 

Оператор throw

 

До сих пор мы рассматривали исключения, которые генерирует среда выпол­нения С#, но это может сделать и сам программист. Для генерации исключения используется оператор throw с параметрам, определяющим вид исключе­ния. Параметр должен быть объектом, порожденным от стандартного класса System.Exception. Этот объект используется для передачи информации об исклю­чении его обработчику. Оператор throw употребляется либо с параметром, либо без него:

 

throw [ выражение ];

 

Форма без параметра применяется только внутри блока catch для повторной ге­нерации исключения. Тип выражения, стоящего после throw, определяет тип ис­ключения, например:

 

throw new DivideByZeroException ();

 

Здесь после слова throw записано выражение, создающее объект стандартного класса «ошибка при делении на 0» с помощью операции new. При генерации исключения выполнение текущего блока прекращается и проис­ходит поиск соответствующего обработчика с передачей ему управления. Обра­ботчик считается найденным, если тип объекта, указанного после throw, либо тот же, что задан в параметре catch, либо является производным от него.

 Рассмотрим пример, приведенный в спецификации С#:

 

 

 

В методе F выполняется промежуточная обработка исключения, которая заклю­чается в том, что на консоль выводится поле Message перехваченного объекта е (об элементах класса Exception рассказывается в следующем разделе). После этого исключение генерируется заново. Несмотря на то что в обработчике исключения создается новый объект класса Exception с измененной строкой информации,  передаваемой в исключении, выбрасывается не этот объект, а тот, который был перехвачен обработчиком, поэтому результат работы программы следующий:

 

Exception in F: G

Exception in Main: G

Заменим оператор throw таким оператором:

throw e;

В этом случае в обработчике будет выброшено исключение, созданное в преды­дущем операторе, и вывод программы изменится:

Exception in F: G

Exception in Main: F

С обработкой исключений мы еще не раз встретимся в примерах программ, при­веденных в следующих главах.

 

 

Класс Exception

 

Класс Exception содержит несколько полезных свойств, с помощью которых мож­но получить информацию об исключении. Они перечислены в табл. 4.2.

 

 

 

Операторы checked и unchecked

 

Как уже упоминалось в главе 3, процессом генерации исключений, возникающих при переполнении, можно управлять с помощью ключевых слов checked и unchecked, которые употребляются как операции, если они используются в выражениях, и как операторы, если они предваряют блок, например:                                                       

 

а = checked (b + с);                         // для выражения (проверка включена)

unchecked {                                     // для блока операторов (проверка выключена)

            а = b + с; }

 

Проверка не распространяется на функции, вызванные в блоке.

 

Рекомендации по программированию

 

К настоящему моменту вы изучили основные операторы С# и можете писать на этом языке простые программы, используя класс, состоящий из одного метода. Даже на этом уровне важно придерживаться определенных правил, следование которым поможет вам избежать многих распространенных ошибок. Конечно, на все случаи жизни советы дать невозможно, ведь не зря многие считают программирование ис­кусством. С приобретением опыта вы добавите к приведенным далее правилам мно­жество своих, выстраданных в результате долгих бдений над «последней» ошибкой.

ВНИМАНИЕ--------------------------------------------------------------------------------------------------

Главная цель, к которой нужно стремиться, — получить понятную программу как можно более простой структуры. В конечном счете, все технологии программирова­ния направлены на достижение именно этой цели, поскольку только так можно до­биться надежности программы и легкости ее модификации.

-------------------------------------------------------------------------------------------------------------------

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

Следующий шаг — записать на естественном языке (возможно, с применением обобщенных блок-схем), что именно и как должна делать программа. Если вы не

можете сформулировать алгоритм пo-pyсски, велика вероятность того, что он плохо продуман (естественно, А не имею в виду, что надо «проговаривать» все на уровне от­дельных операторов, например, «изменяя индекс от 0 до 100 с шагом 1...»). Описание алгоритма полезно по нескольким причинам: оно помогает в деталях продумать алго­ритм, найти на самой ранней стадии некоторые ошибки, разбить программу на ло­гическую последовательность блоков, а также обеспечить комментарии к программе.

При кодировании программы необходимо помнить о принципах структурного программирования: программа должка состоять из четкой последовательности блоков — базовых конструкций, каждая из которых имеет один вход и один вы­ход. Конструкции могут вкладываться, но не пересекаться.

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

Для записи каждого фрагмента алгоритма необходимо использовать наиболее подходящие средства языка. Например, ветвление на насколько направлений по значению целой или строковой переменной эффектнее записать с помощью од­ного оператора switch, а не нескольких операторов if. Для просмотра массива лучше пользоваться циклом for или foreach. Оператор goto применяют весьма редко, например, в операторе выбора switch или для принудительного выхода из нескольких вложенных циклов, а в большинстве других ситуаций лучше исполь­зовать другие средства языка, такие как break или return.

Для организации циклов пользуйтесь наиболее подходящим оператором. Цикл do применяется только в тех случаях, когда тело в любом случае потребуется вы­полнить хотя бы один раз, например, при проверке ввода. При использовании циклов надо стремиться объединять инициализацию, проверку условия выхода и приращение в одном месте. Рекомендации по выбору наиболее подходящего оператора цикла были приведены на с 82.

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

Более короткую ветвь if лучше поместить сначала, иначе вся структура может не поместиться на экране, что затруднит отладку.

Бессмысленно использовать проверку на равенство true или false:

 

 

 

Необходимо предусматривать печать сообщений или генерацию исключения в тех точках программы, куда управление при нормальной работе программы передавать­ся не должно. Именно это сообщение вы с большой вероятностью получите при первом же запуске программы. Например, полезно, если оператор switch имеет ветвь default, реализующую обработку ситуации по умолчанию, если в ней пере­числены все возможные значения переключателя.

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

Генерируйте исключения в тех случаях, когда в месте возникновения ошибки не­достаточно данных, чтобы ее обработать.

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

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

 

пытаться оптимизировать все, что «попадается под руку», поскольку главный принцип программиста тот же, что и врача: «Не навреди!». Если программа ра­ботает недостаточно ^эффективно, надо в первую очередь подумать о том, какие алгоритмы в нее заложены.

Подходить к написанию программы нужно таким образом, чтобы ее можно было в любой момент передать другому программисту. Полезно дать почитать свою программу кому-нибудь из друзей или коллег (а еще лучше — врагов или завист­ников) и в тех местах, которые они не смогут понять без устных комментариев, внести их прямо в текст программы.

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

Программа, если она используется, живет не один год, потребность в каких-то ее новых свойствах проявляется сразу же после ввода в эксплуатацию, и сопровож­дение программы занимает гораздо больше времени, чем ее написание. Основная часть документации должна находиться в тексте программы. Хорошие коммента­рии написать почти так же сложно, как и хорошую программу. В С# есть мощные средства документирования программ, речь о которых пойдет в главе 15, но само содержание комментариев никто, кроме вас, задать не сможет.

Комментарии должны представлять собой правильные предложения без сокраще­ний и со знаками препинания и не должны подтверждать очевидное (коммента­рии в этой книге не могут служить образцом, поскольку они предназначены для обучения, а не сопровождения). Например, бессмысленны фразы типа «вызов ме­тода f» или «описание переменных». Если комментарий к фрагменту программы занимает несколько строк, разместить его лучше перед фрагментом, чем справа от него, и выровнять по вертикали.

Вложенные блоки должны иметь отступ в 3-5 символов, причем блоки одного уровня вложенности должны быть выровнены по вертикали. Форматируйте текст по столбцам везде, где это возможно, это делает программу гораздо понятнее:

 

 

 

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

Абзацный отступ комментария должен соответствовать отступу комменти­руемого блока:

 

 

Для разделения методов и других логически законченных фрагментов пользуй­тесь пустыми строками или комментарием вида

//---------------------------------------------------------------------

Не следует размещать в одной строке много операторов. Как и в русском языке, после знаков препинания должны использоваться пробелы:

 

     f=a+b;                             // плохо! Лучше f = а + b;

 

Помечайте конец длинного составного оператора, например:

 

 

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

В заключение порекомендую тем, кто предпочитает учиться программированию не только на своих ошибках, очень полезные книги [2], [6].

 

Глава 5

Классы: основные понятия

 

Понятие о классах вы получили в разделах «Классы» (см. с. 13) и «Заготовка консольной программы» (см. с. 17). Все программы, приведенные в этой книге ранее, состояли из одного класса с одним-единственным методом Main. Сейчас настало время подробнее изучить состав, правила создания и использования классов. По сути, отныне все, что мы будем рассматривать, так или иначе связа­но с этим ключевым средством языка.

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

Описание класса содержит ключевое слово class, за которым следует его имя, а да­лее в фигурных скобках — тело класса, то есть список его элементов. Кроме того, для класса можно задать его базовые классы (предки) и ряд необязательных ат­рибутов и спецификаторов, определяющих различные характеристики класса:

 

[ атрибуты ] [ спецификаторы ] class имя_класса [ : предки ]

            Тело_класса

 

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

 

class Demo {}

 

ПРИМЕЧАНИЕ ------------------------------------------------------------------------------------------

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

 

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

 

 

Спецификаторы 2-6 называются спецификаторами доступа. Они определяют, откуда можно непосредственно обращаться к данному классу. Спецификаторы доступа могут присутствовать в описании только в вариантах, приведенных в таб­лице, а также могут комбинироваться с остальными спецификаторами.

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

Класс является обобщенным понятием, определяющим характеристики и пове­дение некоторого множества конкретных объектов этого класса, называемых эк­земплярами, или объектами, класса.

Объекты создаются явным или неявным образом, то есть либо программистом, либо системой. Программист создает экземпляр класса с помощью операции new, например:

Demo a = new Demo();                 // создание экземпляра класса Demo

Demo b = new Demo();                 // создание другого экземпляра класса Demo

 

 

 

ПРИМЕЧАНИЕ-----------------------------------------------------------------------------------------------

Как вы помните, класс относится к ссылочным типам данных, память под которые выделяется в хипе (см. раздел «Типы-значения и ссылочные типы» на с. 35). Таким образом, переменные х и у хранят не сами объекты, а ссылки на объекты, то есть их адреса. Если достаточный для хранения объекта объем памяти выделить не уда­лось, операция new генерирует исключение OutOfMemoryException. Рекомендуется предусматривать обработку этого исключения в программах, работающих с объек­тами большого объема.

--------------------------------------------------------------------------------------------------------------------

 

Для каждого объекта при его создании в памяти выделяется отдельная область, в которой хранятся его данные. Кроме того, в классе могут присутствовать ста­тические элементы, которые существуют в единственном экземпляре для всех объектов класса. Часто статические данные называют данными класса, а осталь­ные — данными экземпляра.

Функциональные элементы класса не тиражируются, то есть всегда хранятся в единственном экземпляре. Для работы с данными класса используются мето­ды класса {статические методы), для работы с данными экземпляра — методы экземпляра, или просто методы.

До сих пор мы использовали в программах только один вид функциональных элементов класса — методы. Поля и методы являются основными элементами класса. Кроме того, в классе можно задавать целую гамму других элементов: свойства, события, индексаторы, операции, конструкторы, деструкторы, а также типы (рис. 5.1).

Ниже приведено краткое описание всех элементов класса (см. также рис. 5.1):

□  Константы класса хранят неизменяемые значения, связанные с классом.

□  Поля содержат данные класса.

□  Методы реализуют вычисления или другие действия, выполняемые классом или экземпляром.

□   Свойства определяют характеристики класса в совокупности со способами их задания и получения, то есть методами записи и чтения.

□  Конструкторы реализуют действия по инициализации экземпляров или клас­са в целом.

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

□  Индексаторы обеспечивают возможность доступа к элементам класса по их порядковому номеру.

 

 

□   Операции задают действия с объектами с помощью знаков операций.

□   События определяют уведомления, которые может генерировать класс.

□   Типы — это типы данных, внутренние по отношению к классу.

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

 

Присваивание и сравнение объектов

 

Операция присваивания рассматривалась в разделе «Операции присваивания» (см. с. 57). Механизм выполнения присваивания один и тот же для величин любого типа, как ссылочного, так и значимого, однако результаты различаются. При присваивании значения копируется значение, а при присваивании ссыл­ки — ссылка, поэтому после присваивания одного объекта другому мы получим две ссылки, указывающие на одну и ту же область памяти (рис. 5.2). Рисунок иллюстрирует ситуацию, когда было создано три объекта, a, b и с, а за­тем выполнено присваивание b = с. Старое значение b становится недоступным и очищается сборщиком мусора. Из этого следует, что если изменить значение

одной величины ссылочного типа, это может отразиться на другой (в данном слу­чае, если изменить объект через ссылку с, объект b также изменит свое значение).

 

 

Аналогичная ситуация с операцией проверки на равенство. Величины значимого типа равны, если равны их значения. Величины ссылочного типа равны, если они ссылаются на одни и те же данные (на рисунке объекты b и с равны, но а не равно b даже при равенстве их значений или если они обе равны null).

 

Данные: поля и константы

 

Данные, содержащиеся в классе, могут быть переменными или константами и задаются в соответствии с правилами, рассмотренными в разделе «Перемен­ные» (см. с. 38) и «Именованные константы» (см. с. 41). Переменные, описанные в классе, называются полями класса.

При описании элементов класса можно также указывать атрибуты и специфика­торы, задающие различные характеристики элементов. Синтаксис описания эле­мента данных приведен ниже:

 

[ атрибуты ] [ спецификаторы ] [ const ] тип имя [ = начальное_значение ]

 

До атрибутов мы доберемся еще не скоро, в главе 12, а возможные специфика­торы полей и констант перечислены в табл. 5.2. Для констант можно использо­вать только спецификаторы 1-6.

 

 

 

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

ВНИМАНИЕ--------------------------------------------------------------------------------------------------

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

---------------------------------------------------------------------------------------------------------------------

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

 

 

 

Поле у вывести на экран аналогичным образом не удастся: оно является закры­тым, то есть недоступно извне (из класса Classl). Поскольку значение этому полю явным образом не присвоено, среда присваивает ему значение ноль.

ВНИМАНИЕ --------------------------------------------------------------------------------------------

Все поля сначала автоматически инициализируются нулем соответствующего типа (например, нолям типа int присваивается 0, а ссылкам на объекты — значение null). После этого полю присваивается значение, заданное при его явной инициа­лизации. Задание начальных значений для статических полей выполняется при инициализации класса, а обычных — при создании экземпляра.

--------------------------------------------------------------------------------------------------------------------

Поля со спецификатором readonly предназначены только для чтения. Установить значение такого поля можно либо при его описании, либо в конструкторе (кон­структоры рассматриваются далее в этой главе).

 

Методы

 

Метод — это функциональный элемент класса, который реализует вычисления или другие действия, выполняемые классом или экземпляром. Методы опреде­ляют поведение класса.

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

Синтаксис метода:

 

[ атрибуты ] [ спецификаторы ] тип имя_метода ( [ параметры ] )

            тело_метода

 

Рассмотрим основные элементы описания метода. Первая строка представляет собой заголовок метода. Тело метода, задающее действия, выполняемые методом, чаще всего представляет собой блок — последовательность операторов в фигур­ных скобках.

При описании методов можно использовать спецификаторы 1-7 из табл. 5.2, имеющие тот же смысл, что и для полей, а также спецификаторы virtual, sealed, override, abstract и extern, которые будут рассмотрены по мере необходимости. Чаще всего для методов задается спецификатор доступа public, ведь методы составляют интерфейс класса — то, с чем работает пользователь, поэтому они должны быть доступны.

 

ВНИМАНИЕ-----------------------------------------------------------------------------------------------

Статические (Static) методы, или методы класса, можно вызывать, не создавая эк­земпляр объекта. Именно таким образом используется метод Main.

 

 

Пример простейшего метода:

public double Gety ()                     // метод для получения поля у из листинга 5.1

{                                                                 .                          

            return у;

}

 

Тип определяет, значение какого типа вычисляется с помощью метода. Часто употребляется термин «метод возвращает значение», поскольку после выполне­ния метода происходит возврат в то место вызывающей функции, откуда был вызван метод, и передача, туда значения выражения, записанного в операторе return (рис. 5.3). Если метод не возвращает никакого значения, в его заголовке задается тип void, а оператор return отсутствует.

Параметры используются для обмена информацией с методом. Параметр пред­ставляет собой локальную переменную, которая при вызове метода принима­ет значение соответствующего аргумента. Область действия параметра — весь метод.                                                                                                          

Например, чтобы вычислить значение синуса для вещественной величины х, мы передаем ее в качестве аргумента в метод Sin класса Math, а чтобы вывести значе­ние этой переменной на экран, мы передаем ее в метод WriteLine класса Console:

double x = 0.1;

double у = Math.Sin(x);

Console.WriteLine(x);

 

При этом метод Sin возвращает в точку своего вызова вещественное значение си­нуса, которое присваивается переменной у, а метод WriteLine ничего не возвращает.

ВНИМАНИЕ -------------------------------------------------------------------------------------------------

Метод, не возвращающий значение, вызывается отдельным оператором, а метод, возвращающий значение, — в составе выражения в правой части оператора при­сваивания.

-------------------------------------------------------------------------------------------------------------------

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

соответствовать друг другу. Правила соответствия подробно рассматриваются в следующих разделах.

Для каждого параметра должны задаваться его тип и имя. Например, заголовок метода

Sin выглядит следующим образом:

 

public static double Sin( double a );

 

Имя метода вкупе с количеством, типами и спецификаторами его параметров представляет собой сигнатуру метода — то, по чему один метод отличают от других. В классе не должно быть методов с одинаковыми сигнатурами. В листинге 5.2 в класс Demo добавлены методы установки и получения значения поля у (на самом деле для подобных целей используются не методы, а свойства, которые рассматриваются чуть позже). Кроме того, статическое поле s закрыто, то есть определено по умолчанию как private, а для его получения описан метод Gets, являющий собою пример статического метода.              

                       

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

ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------

При вызове метода из другого метода того же класса имя класса/экземпляра можно не указывать.

---------------------------------------------------------------------------------------------------------------------

Параметры методов

 

Рассмотрим более подробно, каким образом метод обменивается информацией с вызвавшим его кодом. При вызове метода выполняются следующие действия:

1.  Вычисляются выражения, стоящие на месте аргументов.

2.  Выделяется память под параметры метода в соответствии с их типом.

3.  Каждому из параметров сопоставляется соответствующий аргумент (аргумен­ты как бы накладываются на параметры и замещают их).

4.  Выполняется тело метода.

5.  Если метод возвращает значение, оно передается в точку вызова; если метод имеет тип void, управление передается на оператор, следующий после вызова.

При этом проверяется соответствие типов аргументов и параметров и при необ­ходимости выполняется их преобразование. При несоответствии типов выдается диагностическое сообщение. Листинг 5.3 иллюстрирует этот процесс.

 

 

 

 

 

В классе описан метод Мах, который выбирает наибольшее из двух переданных ему значений. Параметры описаны как а и Ь. В методе Main выполняются три вы­зова Мах. В результате первого вызова методу Мах передаются два аргумента того же типа, что и параметры, во втором вызове — аргументы совместимого тип в третьем — выражения.

ВНИМАНИЕ--------------------------------------------------------------------------------------------

Главное требование при передаче параметров состоит в том, что аргументы при вызове метода должны записываться в том же порядке, что и в заголовке метода, и должно существовать неявное преобразование типа каждого аргумента к типу со­ответствующего параметра.

----------------------------------------------------------------------------------------------------------------

Количество аргументов должно соответствовать количеству параметров. На име­на никаких ограничений не накладывается: имена аргументов могут как совпа­дать, так и не совпадать с именами параметров.

Существуют два способа передачи параметров: по значению и по ссылке.

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

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

В С# для обмена данными между вызывающей и вызываемой функциями преду­смотрено четыре типа параметров:

□   параметры-значения;

□   параметры-ссылки — описываются с помощью ключевого слова ref;

□   выходные параметры — описываются с помощью ключевого слова out;

□   параметры-массивы — описываются с помощью ключевого слова params.

Ключевое слово предшествует описанию типа параметра. Если оно опущено, па­раметр считается параметром-значением. Параметр-массив может быть только один и должен располагаться последним в списке, например:

 

public int Calculate( int a, ref int b, out int c, params int[ ] d ) ...

 

 

О параметрах-массивах мы будем говорить позже, в главе 7 (см. с. 154), а сейчас рассмотрим остальные типы параметров.

 

Параметры-значения

 

Параметр-значение описывается в заголовке метода следующим образом:

тип им

Пример заголовка метода, имеющего один параметр-значение целого типа:

 

void P( int х )

 

Имя параметра может быть произвольным. Параметр х представляет собой ло­кальную переменную, которая получает свое значение из вызывающей функции при вызове метода. В метод передается копия значения аргумента. Механизм передачи следующий: из ячейки памяти, в которой хранится перемен­ная, передаваемая в метод, берется ее значение и копируется в специальную об­ласть памяти — область параметров. Метод работает с этой копией, следователь­но, доступа к ячейке, где хранится сама переменная, не имеет. По завершении работы метода область параметров освобождается. Таким образом, для парамет­ров-значений используется, как вы догадались, передача по значению. Ясно, что этот способ годится только для величин, которые не должны измениться после выполнения метода, то есть для его исходных данных.

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

Например, пусть в вызывающей функции описаны переменные и им до вызова метода присвоены значения:

 

int X = 1;

byte с = 1;

ushort у - 1;

 

Тогда следующие вызовы метода Р, заголовок которого был описан ранее, будут синтаксически правильными:

Р( х );       Р( с ):       Р( у );       Р( 200 ):       Р( х / 4 + 1 );

 

Параметры-ссылки

 

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

требуется изменить значение каких-либо передаваемых в него величин. В этих случаях используются параметры-ссылки.

Признаком параметра-ссылки является ключевое слово ref перед описанием па­раметра:

ref тип имя

 

Пример заголовка метода, имеющего один параметр-ссылку целого типа:

 

void P( ref int x )

 

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

ВНИМАНИЕ -------------------------------------------------------------------------------------------------

При вызове метода на месте параметра-ссылки может находиться только ссылка на инициализированную переменную точно того же типа. Перед именем параметра указывается ключевое слово ref.

-------------------------------------------------------------------------------------------------------------------

Исходные данные передавать в метод по ссылке не рекомендуется, чтобы исклю­чить возможность их непреднамеренного изменения.

Проиллюстрируем передачу параметров-значений и параметров-ссылок на при­мере (листинг 5.4).

 

 

Результаты работы этой программы:

до вызова  2 4

внутри метода 44 33

после вызова   2 33

 

Как видите, значение переменной а в функции Main не изменилось, поскольку пе­ременная передавалась по значению, а значение переменной b изменилось пото­му, что она была передана по ссылке.

Несколько иная картина получится, если передавать в метод не величины значи­мых типов, а экземпляры классов, то есть величины ссылочных типов. Как вы помните, переменная-объект на самом деле хранит ссылку на данные, располо­женные в динамической памяти, и именно эта ссылка передается в метод либо по адресу, либо по значению. В обоих случаях метод получает в свое распоряже­ние фактический адрес данных и, следовательно, может их изменить.

СОВЕТ ----------------------------------------------------------------------------------------------

Для простоты можно считать, что объекты всегда передаются по ссылке.

-----------------------------------------------------------------------------------------------------------

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

 

Выходные параметры

 

Довольно часто возникает необходимость в методах, которые формируют не­сколько величин, например, если в методе создаются объекты или инициализи­руются ресурсы. В этом случае становится неудобным ограничение параметров-ссылок: необходимость присваивания значения аргументу до вызова метода. Это ограничение снимает спецификатор out. Параметру, имеющему этот специфика­тор, должно быть обязательно присвоено значение внутри метода, компилятор за этим следит. Зато в вызывающем коде можно ограничиться описанием пере­менной без инициализации.

Изменим описание второго параметра в листинге 5.4 так, чтобы он стал выход­ным (листинг 5.5).

 

При вызове метода перед соответствующим параметром тоже указывается клю­чевое слово out.

СОВЕТ ---------------------------------------------------------------------------------------------------------

В списке параметров записывайте сначала все входные параметры, затем — все ссылки и выходные параметры. Давайте параметрам имена, по которым можно по­лучить представление об их назначении.

---------------------------------------------------------------------------------------------------------------------

 

Ключевое слово this

 

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

В явном виде параметр this применяется для того, чтобы возвратить из метода ссылку на вызвавший объект, а также для идентификации поля в случае, если его имя совпадает с именем параметра метода, например:

 

 

 

 

Конструкторы

 

Конструктор предназначен для инициализации объекта. Он вызывается авто­матически при создании объекта класса с помощью операции new. Имя конст­руктора совпадает с именем класса. Ниже перечислены свойства конструкторов:

□ Конструктор не возвращает значение, даже типа void.

□   Класс может иметь несколько конструкторов с разными параметрами для раз­ных видов инициализации.

□   Если программист не указал ни одного конструктора или какие-то поля не были инициализированы, полям значимых типов присваивается нуль, полям ссылочных типов — значение null.

□   Конструктор, вызываемый без параметров, называется конструктором по умолчанию.

До сих пор мы задавали начальные значения полей класса при описании класса (см., например, листинг 5.1). Это удобно в том случае, когда для всех экземпляров класса начальные значения некоторого поля одинаковы. Если же при создании объектов требуется присваивать полю разные значения, это следует делать в кон­структоре. В листинге 5.6 в класс Demo добавлен конструктор, а поля сделаны закрытыми (ненужные в данный момент элементы опущены). В программе соз­даются два объекта с различными значениями полей.

 

 

 

Часто бывает удобно задать в классе несколько конструкторов, чтобы обес­печить возможность инициализации объектов разными способами. Следую­щий пример несколько «притянут за уши», но тем не менее иллюстрирует этот тезис:

 

 

Все конструкторы должны иметь разные сигнатуры.

 

Если один из конструкторов выполняет какие-либо действия, а другой должен делать то же самое плюс еще что-нибудь, удобно вызвать первый конструктор из второго. Для этого используется уже известное вам ключевое слово this в дру­гом контексте, например:

 

 

Конструкция, находящаяся после двоеточия, называется инициализатором, то есть тем кодом, который исполняется до начала выполнения тела конст­руктора.

Как вы помните, все классы в С# имеют общего предка — класс object. Конст­руктор любого класса, если не указан инициализатор, автоматически вызывает конструктор своего предка. Это можно сделать и явным образом с помощью

ключевого слова base, обозначающего конструктор базового класса. Таким обра­зом, первый конструктор из предыдущего примера можно записать и так:

public Demo( int a )  : base()                          // конструктор 1

{

            this.a = а;

}

 

ПРИМЕЧАНИЕ ----------------------------------------------------------------------------------------------

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

---------------------------------------------------------------------------------------------------------------------

До сих пор речь шла об «обычных» конструкторах, или конструкторах экземп­ляра. Существует второй тип конструкторов — статические конструкторы, или конструкторы класса. Конструктор экземпляра инициализирует данные экземп­ляра, конструктор класса — данные класса.

Статический конструктор не имеет параметров, его нельзя вызвать явным об­разом. Система сама определяет момент, в который требуется его выполнить. Гарантируется только, что это происходит до создания первого экземпляра объ­екта и до вызова любого статического метода.

Некоторые классы содержат только статические данные, и, следовательно, созда­вать экземпляры таких объектов не имеет смысла. Чтобы подчеркнуть этот факт, в первой версии С# описывали пустой закрытый (private) конструктор. Это предотвращало попытки создания экземпляров класса. В листинге 5.7 приведен пример класса, который служит для группировки величин. Создавать экземпля­ры этого класса запрещено.

 

 

 

 

ПРИМЕЧАНИЕ-----------------------------------------------------------------------------------------------

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

--------------------------------------------------------------------------------------------------------------------

В версию 2.0 введена возможность описывать статический класс, то есть класс с модификатором static. Экземпляры такого класса создавать запрещено, и кроме того, от него запрещено наследовать. Все элементы такого класса должны явным образом объявляться с модификатором static (константы и вложенные типы классифицируются как статические элементы автоматически). Конструктор эк­земпляра для статического класса задавать, естественно, запрещается. В листинге 5.8 приведен пример статического класса.

 

 

 

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

 

 

 

Результат работы программы:

 

Monster Noname   health  = 100 ammo = 100

Monster Vasia        health  = 100 ammo = 100 Monster

Masha                     health = 200 ammo = 200

 

В классе три закрытых поля (name, health и ammo), четыре метода (GetName, GetHealth, GetAmmo и Passport) и три конструктора, позволяющие задать при создании объек­та ни одного, один или три параметра.

 

Свойства

 

Свойства служат для организации доступа к полям класса. Как правило, свойст­во связано с закрытым полем класса и определяет методы его получения и уста­новки. Синтаксис свойства:

 

[ атрибуты ] [ спецификаторы ] тип имя_свойства

{

     [ get код доступа]

     [ set код доступа]

}

 

Значения спецификаторов для свойств и методов аналогичны. Чаще всего свой­ства объявляются как открытые (со спецификатором publiс), поскольку они вхо­дят в интерфейс объекта.

Код доступа представляет собой блоки операторов, которые выполняются при получении (get) или установке (set) свойства. Может отсутствовать либо часть get, либо set, но не обе одновременно.

Если отсутствует часть set, свойство доступно только для чтения (read-only), если отсутствует часть get, свойство доступно только для записи (write-only).

В версии С# 2.0 введена удобная возможность задавать разные уровни доступа для частей get и set. Например, во многих классах возникает потребность обеспе­чить неограниченный доступ для чтения и ограниченный — для записи.

Спецификаторы доступа для отдельной части должны задавать либо такой же, либо более ограниченный доступ, чем спецификатор доступа для свойства в целом. На­пример, если свойство описано как public, его части могут иметь любой специфика­тор доступа, а если свойство имеет доступ protected internal, его части могут объяв­ляться как internal, protected или private. Синтаксис свойства в версии 2.0 имеет вид

 

[ атрибуты ] [ спецификаторы ] тип имя_свойства

{

 [ [ атрибуты ] [ спецификаторы ] get код_доступа ]

[ [ атрибуты ] [ спецификаторы ] set код_доступа ]

 

ПРИМЕЧАНИЕ ----------------------------------------------------------------------------------------------

Двоеточие между именами Button и Control в заголовке класса Button означает, что класс Button является производным от класса Control.,

--------------------------------------------------------------------------------------------------------------------

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

В программе свойство выглядит как поле класса, например:

 

Button ok = new Button();

ok.Caption = "OK";                                 // вызывается метод установки свойства

string s = ok.Caption;                            // вызывается метод получения свойства

 

 

При обращении к свойству автоматически вызываются указанные в нем методы чтения и установки.

Синтаксически чтение и запись свойства выглядят почти как методы. Метод get должен содержать оператор return, возвращающий выражение, для типа которо­го должно существовать неявное преобразование к типу свойства. В методе set используется параметр со стандартным именем value, который содержит уста­навливаемое значение.                     

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

С помощью свойств можно отложить инициализацию поля до того момента, ко­гда оно фактически потребуется, например:                        

 

Добавим в класс Monster, описанный в листинге 5.9, свойства, позволяющие рабо­тать с закрытыми полями этого класса. Свойство Name сделаем доступным только для чтения, поскольку имя объекта задается в конструкторе и его изменение не предусмотрено, в свойствах Health и Ammo введем проверку на положительность устанавливаемой величины. Код класса несколько разрастется, зато упростится его использование.

 

 

 

 

 

 

Результат работы программы:

 

Monster Masha       health = 200 ammo = 200

Monster Masha       health = 199 ammo = 300

 

Рекомендации по программированию

 

При создании класса, то есть нового типа данных, следует хорошо продумать его интерфейс — средства работы с классом, доступные использующим его програм­мистам. Интерфейс хорошо спроектированного класса интуитивно ясен, непро­тиворечив и обозрим. Как правило, он не должен включать поля данных.

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

Не нужно расширять интерфейс класса без необходимости, «на всякий слу­чай», поскольку увеличение количества методов затрудняет понимание класса пользователем. В идеале интерфейс должен быть полным, то есть предоставлять возможность выполнять любые разумные действия с классом, и одновременно минимально необходимым — без дублирования и пересечения возможностей ме­тодов.

Методы определяют поведение класса. Каждый метод класса должен решать только одну задачу (не надо объединять два коротких независимых фрагмента кода в один метод). Размер метода может варьироваться в широких пределах, все зависит от того, какие функции он выполняет. Желательно, чтобы тело метода помещалось на 1-2 экрана: одинаково сложно разбираться в программе, содер­жащей несколько необъятных функций, и в россыпи из сотен единиц по не­сколько строк каждая.

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

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

Предпочтительнее, чтобы каждая функция вычисляла ровно один результат, одна­ко это не всегда оправдано. Если величина вычисляется внутри функции и воз­вращается из нее через список параметров, необходимо использовать перед соот­ветствующим параметром ключевое слово out. Если параметр значимого типа может изменить свою величину внутри функции, его предваряют ключевым сло­вом ref. Величины ссылочного типа всегда передаются по адресу и, следователь­но, могут изменить внутри функции свое значение.

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

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

При написании кода методов следует руководствоваться правилами, приведен­ными в аналогичном разделе главы 4 (см. с. 95).

 

 

Глава 6

Массивы и строки

 

В этой главе рассматриваются элементы, без которых не обходится практически ни одна программа, — массивы и строки.

 

Массивы

 

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

Элементы массива имеют одно и то же имя, а различаются порядковым номером (индексом). Это позволяет компактно записывать множество операций с помо­щью циклов.

Массив относится к ссылочным типам данных, то есть располагается в динами­ческой области памяти, поэтому создание массива начинается с выделения памя­ти под его элементы. Элементами массива могут быть величины как значимых, так и ссылочных типов (в том числе массивы). Массив значимых типов хранит значения, массив ссылочных типов — ссылки на элементы. Всем элементам при создании массива присваиваются значения по умолчанию: нули для значимых типов и null — для ссылочных.

На рис. 6.1 представлен массив, состоящий из пяти элементов любого значимого типа, например int или double, а рис. 6.2 иллюстрирует организацию массива из элементов ссылочного типа.

Вот, например, как выглядят операторы создания массива из 10 целых чисел и массива из 100 строк:

Int     [ ]      w - new int[10];

string [ ] z = new string[100];

 

 

 

 

В первом операторе описан массив w типа int[].Операция new выделяет память под 10 целых элементов, и они заполняются нулями.

Во втором операторе описан массив z типа string[].Операция new выделяет память под 100 ссылок на строки, и эти ссылки заполняются значением null. Память под сами строки, составляющие массив, не выделяется — это будет необ­ходимо сделать перед заполнением массива.

Количество элементов в массиве (размерность) не является частью его типа, это количество задается при выделении памяти и не может быть изменено впоследствии. Размерность может задаваться не только константой, но и вы­ражением. Результат вычисления этого выражения должен быть неотрица­тельным, а его тип должен иметь неявное преобразование к int, uint, long или ulong. Пример размерности массива, заданной выражением:

 

short n - ...;

string[] z = new string[n + 1];

 

Элементы массива нумеруются с нуля, поэтому максимальный номер элемен­та всегда на единицу меньше размерности (например, в описанном выше мас­сиве w элементы имеют индексы от 0 до 9). Для обращения к элементу массива после имени массива указывается номер элемента в квадратных скобках, на­пример:

 w[4]            z[i]

 

С элементом массива можно делать все, что допустимо для переменных того же типа. При работе с массивом автоматически выполняется контроль выхода за его границы: если значение индекса выходит за границы массива, генерируется ис­ключение IndexOutOfRangeException.

Массивы одного типа можно присваивать друг другу1. При этом происходит присваивание ссылок, а не элементов, как и для любого другого объекта ссылочного типа, например:

int[] a = new int[1.0];

int[] b = a;                        // b и а указывают на один и тот же массив

Все массивы в С# имеют общий базовый класс Array, определенный в пространстве имен System. В нем есть несколько полезных методов, упрощающих работу с мас­сивами, например методы получения размерности, сортировки и поиска. Мы рас­смотрим эти методы немного позже в разделе «Класс System.Array» (см. с. 133).

ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------Массивы, являющиеся полями класса, могут иметь те же спецификаторы, что и поля, представляющие собой простые переменные.

-------------------------------------------------------------------------------------------------------------------

В С# существуют три разновидности массивов: одномерные, прямоугольные и ступенчатые (невыровненные).

 

Одномерные массивы

 

Одномерные массивы используются в программах чаще всего. Варианты описа­ния массива:

 

Тип []  имя;

тип[]   имя = new тип [ размерность ]:

тип[]   имя = { список инициализаторов };

тип[]   имя = new тип [] { список_инициализаторов }:

тип[]   имя = new тип [ размерность ] { список_инициализаторов };

 

ВНИМАНИЕ----------------------------------------------------

При описании массивов квадратные скобки являются элементом синтаксиса, а не указанием на необязательность конструкции.

-----------------------------------------------------------------------------------------------------------

Примеры описаний (один пример для каждого варианта описания):

 

 

Здесь описано пять массивов. Отличие первого, оператора от остальных состоит в том, что в нем, фактически, описана только ссылка на массив, а память под эле­менты массива не выделена. Если список инициализации не задай, размерность может быть не только константой, но и выражением типа, приводимого к целому.

В каждом из остальных массивов по четыре элемента целого типа. Как видно из операторов 3—5, массив при описании можно инициализировать. Если при этом не задана размерность (оператор 3), количество элементов вычисляется по количе­ству инициализирующих значений. Для полей объектов и локальных переменных можно опускать операцию new, она будет выполнена по умолчанию (оператор 2). Если присутствует и размерность, и список инициализаторов, размерность долж­на быть константой (оператор 4).

ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------

Если количество инициализирующих значений не совпадает с размерностью, воз­никает ошибка компиляции.

--------------------------------------------------------------------------------------------------------------------

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

 

Обратите внимание на то, что для вывода массива требуется организовать цикл.

 

Прямоугольные массивы

 

Прямоугольный массив имеет более одного измерения. Чаще всего в программах используются двумерные массивы. Варианты описания двумерного массива:

 

тип[,] имя;

тип[,] имя = new тип [ разм_1, разм_2 ];

тип[,] имя = { список_инициализаторов };

тип[,] имя = new тип [,] { список_инициализаторов };

тип[,] имя = new тип [ разм_1, разм_2 ] { список_инициализаторов };

 

Примеры описаний (один пример для каждого варианта описания):

 

int[,]  a;                                                                //    элементов нет

int[,]  b = new int[2, 3];                                       //   элементы равны 0

int[,]   с = {{1, 2, 3}, {4, 5, 6}};                           // 3    new подразумевается

int[.]   с = new int[, ]{1, 2, 3}, {4, 5, 6}};           // 4    размерность вычисляется     

int[,]   d = new int[2,3] {{1. 2, 3},  {4, 5, 6}};    // 5    избыточное описание

 

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

а[1. 4]           b[i,  j]           b[j,  i]

 

 

 

ВНИМАНИЕ---------------------------------------------------------------------------------------------------

Необходимо помнить, что компилятор воспринимает как номер строки первый ин­декс, как бы он ни был обозначен в программе.

-------------------------------------------------------------------------------------------------------------------

В качестве примера рассмотрим программу, которая для целочисленной матри­цы размером 3x4 определяет среднее арифметическое ее элементов и количест­во положительных элементов в каждой строке (рис. 6.3).

 

 

Для нахождения среднего арифметического элементов массива требуется найти их общую сумму, после чего разделить ее на количество элементов. Порядок пере­бора элементов массива (по строкам или по столбцам) роли не играет. Нахождение количества положительных элементов каждой строки требует просмотра матрицы по строкам. Схема алгоритма приведена на рис. 6.4, программа — в листинге 6.2.

 

 

 

 

 

ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------Для суммирования элементов описана переменная Sum вещественного типа. Если описать ее как целую, при делении на количество элементов будет отброшена дроб­ная часть.

----------------------------------------------------------------------------------------------------------------

Обратите внимание на то, что переменная sum обнуляется перед циклом про­смотра всей матрицы, а количество положительных элементов — перед циклом просмотра очередной строки, поскольку для каждой строки его вычисление на­чинается заново.

 

Ступенчатые массивы

 

В ступенчатых массивах количество элементов в разных строках может различать­ся. В памяти ступенчатый массив хранится иначе, чем прямоугольный: в виде нескольких внутренних массивов, каждый из которых имеет свой размер. Кроме

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

 

 

Описание ступенчатого массива:

тип[ ][ ] имя;

 

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

 

int[ ] [] a = new int[3][];                // выделение памяти под ссылки на три строки

            а[0] = new int[5];             // выделение памяти под 0-ю строку (5 элементов)

            а[1] = new int[3];            // выделение памяти под 1-ю строку (3 элемента)

            а[2] = new int[4];           // выделение памяти под 2-ю строку (4 элемента)

 

Здесь а[0], а[1]и а[2] — это отдельные массивы, к которым можно обращаться по имени (пример приведен в следующем разделе). Другой способ выделения памяти:

 

int[ ][ ] а = { new int[5], new int[3], new int[4] };                                      .

 

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

а[1][2]            a[ i ][ j ]            a[ j ][ i ]

 

В остальном использование ступенчатых массивов не отличается от использова­ния прямоугольных. Невыровненные массивы удобно применять, например, для работы с треугольными матрицами большого объема.

 

Класс System.Array

 

Ранее уже говорилось, что все массивы в С# построены на основе базового клас­са Array, который содержит полезные для программиста свойства и методы, часть из которых перечислены в табл. 6.1.

 

Свойство Length позволяет реализовывать алгоритмы, которые будут работать с массивами различной длины или, например, со ступенчатым массивом. Исполь­зование этого свойства вместо явного задания размерности исключает возмож­ность выхода индекса за границы массива. В листинге 6.3 продемонстрировано применение элементов класса Array при работе с одномерным массивом.

 

 

 

 

 

Методы Sort, IndexOf и BinarySearch являются статическими, поэтому к ним об­ращаются через имя класса, а не экземпляра, и передают в них имя массива. Двоичный поиск можно применять только для упорядоченных массивов. Он выполняется гораздо быстрее, чем линейный поиск, реализованный в методе Index0f. В листинге поиск элемента, имеющего значение 18, выполняется обоими

этими способами.

 

ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------

Рассмотренные методы имеют по несколько версий (в этом случае употребляется термин «перегруженные методы»), что позволяет выполнять поиск, сортировку и копирование как в массиве целиком, как и в его указанном диапазоне.

 

В классе Classl описан вспомогательный статический метод PrintArray, предназна­ченный для вывода массива на экран. В него передаются два параметра: строка заголовка header и массив. Количество элементов массива определяется внутри метода с помощью свойства Length. Таким образом, этот метод можно использо­вать для вывода любого целочисленного одномерного массива.

 

 

Для того чтобы применять метод PrintArray к массивам, состоящим из элементов другого типа, можно описать его второй параметр как Array. Правда, при этом значение элемента массива придется получать с помощью метода GetValue, посколь­ку доступ по индексу для класса Array не предусмотрен. Обобщенный метод вы­вода массива выглядит так:

 

 

В листинге 6.4 продемонстрировано применение элементов класса Array при ра­боте со ступенчатым массивом.

 

 

 

Обратите внимание на то, как внутри цикла по строкам определяется длина каж­дого массива. Результат работы программы:

 

 

Оператор foreach

 

Оператор foreach применяется для перебора элементов в специальным обра­зом организованной группе данных. Массив является именно такой группой. Удобство этого вида цикла заключается в том, что нам не требуется определять количество элементов в группе и выполнять их перебор по индексу: мы про­сто указываем на необходимость перебрать все элементы группы. Синтаксис оператора:

 

foreach ( тип имя in выражение ) тело_цикла

 

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

Например, пусть задан массив:

 

int[ ] а =    {24, 50,  18, 3,  16,  -7, 9,  -1 };

 

Вывод этого массива на экран с помощью оператора foreach выглядит следую­щим образом:

 

foreach (int x in a) Console.WriteLine( x );

 

Этот оператор выполняется так: на каждом проходе цикла очередной элемент массива присваивается переменной х и с ней производятся действия, записанные в теле цикла.

Ступенчатый массив из листинга 6.4 вывести на экран с помощью оператора foreach немного сложнее, чем одномерный, но все же проще, чем с помощью цикла for:

foreach ( int[ ] x in a )

{

            foreach ( int у in x ) Console.Write( "\ t" + у );                                 -

            Console.WriteLine ();

 

}

 

В листинге 6.5 решается та же задача, что и в листинге 6.1, но с использованием цикла foreach. Обратите внимание на то, насколько понятнее стала программа.

 

 

 

 

 

 

Такая запись становится возможной потому, что любой объект может быть неяв­но преобразован к типу его базового класса, а тип object, как вы помните, являет­ся корневым классом всей иерархии. Когда вы продвинетесь в изучении С# до раздела «Виртуальные методы» (см. с. 178), вы поймете механизм выполнения этого кода, а пока можете просто им пользоваться.

ВНИМАНИЕ------------------------------------------------------------------------------------------------

Ограничением оператора foreach является то, что с его помощью можно только просматривать значения в группе данных, но не изменять их.

--------------------------------------------------------------------------------------------------------------------

 

Массивы объектов

 

При создании массива, состоящего из элементов ссылочного типа, память выде­ляется только под ссылки на элементы, а сами элементы необходимо разместить в хипе явным образом. В качестве примера создадим массив из объектов некото­рого класса Monster:

 

 

 

В программе для получения случайных значений использован стандартный класс Random, который описан далее в этой главе (см. с. 148). В операторе 1 выде­ляется пять ячеек памяти под ссылки на экземпляры класса Monster?, и эти ссыл­ки заполняются значением null. В цикле 2 создаются пять объектов: операция new выделяет память в хипе необходимого для хранения полей объекта объема, а конструктор объекта заносит в эти поля соответствующие значения (выполня­ется версия конструктора с тремя параметрами). Цикл 3 демонстрирует удобство применения оператора foreach для работы с массивом.

 

Символы и строки

 

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

 

Символы  

 

Символьный тип char предназначен для хранения символов в кодировке Unicode. Способы представления символов рассматривались в разделе «Литералы» (см. с. 26). Символьный тип относится к встроенным типам данных С# и соответствует стан­дартному классу Char библиотеки .NET из пространства имен System. В этом классе определены статические методы, позволяющие задать вид и категорию символа, а также преобразовать символ в верхний или нижний регистр и в число. Основ­ные методы приведены в табл. 6.2.

 

 

В листинге 6.6 продемонстрировано использование этих методов.

 

 

 

 

 

В операторе 1 описаны три символьных переменных. Они инициализируются символьными литералами в различных формах представления. Далее выполня­ются вывод и преобразование символов.

В цикле 2 анализируется вводимый с клавиатуры символ. Можно вводить и управ­ляющие символы, используя сочетание клавиши Ctrl с латинскими буквами. При вводе использован метод Parse, преобразующий строку, которая должна содер­жать единственный символ, в символ типа char. Поскольку вводится строка, ввод каждого символа следует завершать нажатием клавиши Enter. Цикл выполняет­ся, пока пользователь не введет символ q.

Вывод символа сопровождается его кодом в десятичном виде. Для вывода кода используется явное преобразование к целому типу. Явное преобразование из сим­волов в строки и обратно в С# не существует, неявным же образом любой объект, в том числе и символ, может быть преобразован в строку, например:

 

string s = 'к' + 'о' + 'т';                      // результат - строка "кот"

 

При вводе и преобразовании могут возникать исключительные ситуации, напри­мер, если пользователь введет пустую строку. Для «мягкого» завершения про­граммы предусмотрена обработка исключений.

 

Массивы символов

 

Массив символов, как и массив любого иного типа, построен на основе базового v класса Array, некоторые свойства и методы которого были перечислены в табл. 6.1. Применение этих методов позволяет эффективно решать некоторые задачи. Про­стой пример приведен в листинге 6.7.

 

 

 

 

 

Символьный массив можно инициализировать, либо непосредственно задавая его элементы (оператор 1), либо применяя метод ToCharArray класса string, который разбивает исходную строку на отдельные символы (оператор 2).

 

Строки типа string

 

Тип string, предназначенный для работы со строками символов в кодировке Unicode, является встроенным типом С#. Ему соответствует базовый класс System.String библиотеки .NET.

 

Для строк определены следующие операции:

 

□  присваивание (=);

□  проверка на равенство (= =);

□  проверка на неравенство (!=);

□  обращение по индексу ([]);

□  сцепление (конкатенация) строк (+).

 

Несмотря на то что строки являются ссылочным типом данных, на равенство и неравенство проверяются не ссылки, а значения строк. Строки равны, если име­ют одинаковое количество символов и совпадают посимвольно.

Обращаться к отдельному элементу строки по индексу можно только для полу­чения значения, но не для его изменения. Это связано с тем, что строки типа string относятся к так называемым неизменяемым типам данных. Методы, из­меняющие содержимое строки, на самом деле создают новую копию строки. Не­используемые «старые» копии автоматически удаляются сборщиком мусора.

В классе System.String предусмотрено множество методов, полей и свойств, поз­воляющих выполнять со строками практически любые действия. Основные эле­менты класса приведены в табл. 6.3.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

В операторе 1 выполняются два последовательных вызова методов: метод Substring возвращает подстроку строки s, которая содержит символы исходной строки, на­чиная с третьего. Для этой подстроки вызывается метод Remove, удаляющий из нее два символа, начиная с 12-го. Результат работы метода присваивается пере­менной sub.

Аргументом метода Split (оператор 2) является разделитель, в данном случае -символ пробела. Метод разделяет строку на отдельные слова, которые заносятся в массив строк mas. Статический метод Join (он вызывается через имя класса) объ­единяет элементы массива mas в одну строку, вставляя между каждой парой слов строку "! ". Оператор 3 напоминает вам о том, как вводить строки с клавиатуры.

 

Форматирование строк

 

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

В общем виде параметр задается следующим образом:

 

{п [,т[:спецификатор_формата]]}                                                                                                  

 

Здесь п — номер параметра. Параметры нумеруются с нуля, нулевой параметр заменяется значением первой переменной из списка вывода, первый параметр — второй переменной и т. д. Параметр m определяет минимальную ширину поля, которое отводится под выводимое значение. Если выводимому числу достаточно меньшего количества позиций, неиспользуемые позиции заполняются пробела­ми. Если числу требуется больше позиций, параметр игнорируется.

Спецификатор формата, как явствует из его названия, определяет формат вы­вода значения. Например, спецификатор С (Currency) означает, что параметр должен форматироваться как валюта с учетом национальных особенностей пред­ставления, а спецификатор X (Hexadecimal) задает шестнадцатеричную форму представления выводимого значения.

ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------

До настоящего момента мы пользовались сокращенной записью, задавая только но­мера параметров. Список спецификаторов формата приведен в приложении.

------------------------------------------------------------------------------------------------------------------

В операторе 5 используются так называемые пользовательские шаблоны формати­рования. Если приглядеться, в них нет ничего сложного: после двоеточия задается вид выводимого значения посимвольно, причем на месте каждого символа может стоять либо #, либо 0. Если указан знак #, на этом месте будет выведена цифра числа, если она не равна нулю. Если указан 0, будет выведена любая цифра, в том числе и 0. В табл. 6.4 приведены примеры шаблонов и результатов вывода.

 

 

 

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

 

Строки типа StringBuilder

 

Возможности, предоставляемые классом string, широки, однако требование не­изменности его объектов может оказаться неудобным. В этом случае для работы со строками применяется класс StringBuilder, определенный в пространстве имен System. Text и позволяющий изменять значение своих экземпляров. При создании экземпляра обязательно использовать операцию new и конструк­тор, например:

 

 

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

Если применяется конструктор без параметров (оператор 1), создается пустая строка размера, заданного по умолчанию (16 байт). Другие виды конструкторов задают объем памяти, выделяемой строке, и/или ее начальное значение. На­пример, в операторе 5 объект инициализируется подстрокой длиной 3 символа, начиная с первого (подстрока "wer"). Основные элементы класса StringBuilder приведены в табл. 6.5.

 

 

 

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

 

Класс Random

 

При отладке программ, использующих массивы, удобно иметь возможность ге­нерировать исходные данные, заданные случайным образом. В библиотеке С# на этот случай есть класс Random, определенный в пространстве имен System.

Для получения псевдослучайной последовательности чисел необходимо сначала создать экземпляр класса с помощью конструктора, например:

 

Random a = new Random( );                 // 1

Random b = new Random( 1 );              // 2

 

Есть два вида конструктора: конструктор без параметров (оператор 1) исполь­зует начальное значение генератора, вычисленное на основе текущего времени. В этом случае каждый раз создается уникальная последовательность. Конструк­тор с параметром типа int (оператор 2) задает начальное значение генерато­ра, что обеспечивает возможность получения одинаковых последовательно­стей чисел.

Для получения очередного значения серии пользуются методами, перечислен­ными в табл. 6.6.

 

    

 

 

 

Рекомендации по программированию

 

Используйте для хранения данных массив, если количество однотипных элемен­тов, которые требуется обработать в вашей программе, известно или, по крайней мере, известно максимальное количество таких элементов. В последнем случае память под массив выделяется «по максимуму», а фактическое количество эле­ментов хранится в отдельной переменной, которая вычисляется в программе.

При работе с массивом нужно обязательно предусматривать обработку ис­ключения IndexOutOfRangeException, если индекс для обращения к массиву вы­числяется в программе по формулам, а не задается с помощью констант или счетчиков циклов for.

СОВЕТ ---------------------------------------------------------------------------------------------------------

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

---------------------------------------------------------------------------------------------------------------------

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

При работе со строками необходимо учитывать, что в С# строка типа string яв­ляется неизменяемым типом данных, то есть любая операция изменения строки на самом деле возвращает ее копию. Для изменения строк используется тип StringBuilder. Прежде чем описывать в программе какое-либо действие со стро­ками, полезно посмотреть, нет ли в списке элементов используемого класса под­ходящих методов и свойств.

Для эффективного поиска и преобразования текста в соответствии с заданными шаблонами используются так называемые регулярные выражения, которые рас­смотрены в главе 15.