Читаем C#. Объектно ориентированное программирование полностью

Оператор «меньше»: результатом выражения A

чение true, если значение переменной A меньше, чем значение переменной

B, и false в противном случае

>=

Оператор «больше или равно»: результатом выражения A>=B является

логическое значение true, если значение переменной A не меньше, чем

значение переменной B, и false в противном случае

<=

Оператор «меньше или равно»: результатом выражения A<=B является

логическое значение true, если значение переменной A не больше, чем

значение переменной B, и false в противном случае

Результатом выражения с оператором сравнения является логическое зна-

чение (true или false). Такие выражения могут сами входить, как состав-

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

являются значения логического типа. Логические же операторы перечис-

лены в табл. 3.4.

Таблица 3.4.  Логические операторы C#

Оператор

Описание

&

Оператор логического «и» (бинарный). Результатом выражения A&B является

логическое значение true, если оба логических операнда A и B равны true.

Если хотя бы один из операндов равен false, результатом будет false

|

Оператор логического «или» (бинарный). Результатом выражения A|B

является логическое значение true, если хотя бы один из логических

операндов A и B равен true. Если оба операнда равны false, результатом

будет false

^

Оператор логического «исключающего или» (бинарный). Результатом вы-

ражения A^B является логическое значение true, если один из операндов, A или B, равен true, а другой равен false. Если оба операнда равны true или оба операнда равны false, результатом будет false

продолжение

104

Глава 3. Основы синтаксиса языка C#

Таблица 3.4 (продолжение)

Оператор

Описание

&&

Сокращенная форма логического оператора «и». От обычной формы

логического оператора «и» отличие && состоит в том, что при вычислении

выражения A&&B второй операнд, B, вычисляется, только если первый опе-

ранд, A, равен true. Если первый операнд, A, равен false, то в качестве

результата выражения A&&B возвращается значение false без вычисления

второго операнда, B

||

Сокращенная форма логического оператора «или». От обычной формы

логического оператора «или» отличие || состоит в том, что при вычислении

выражения A||B второй операнд, B, вычисляется, только если первый опе-

ранд, A, равен false. Если первый операнд, A, равен true, то в качестве

результата выражения A||B возвращается значение true без вычисления

второго операнда, B

!

Оператор логического отрицания (унарный). Результатом выражения !A яв-

ляется значение true, если операнд A равен false. Если операнд A равен

true, результатом выражения !A возвращается значение false Идеологически близки к логическим операторам побитовые (поразряд-

ные) операторы. Необходимо лишь сделать две поправки: во-первых, опе-

рации выполняются с парами (для бинарных операторов) битов в побито-

вом представлении операндов и, во-вторых, вместо логического значения

true следует читать 1, а вместо логического значения false следует читать

0. Правда, в этом правиле исключением являются операторы сдвига. По-

битовые операторы перечислены в табл. 3.5.

Таблица 3.5.  Побитовые операторы C#

Оператор

Описание

&

Оператор поразрядного «и». Сопоставляются соответствующие биты двух

чисел. Если оба сопоставляемых бита равны единице, на выходе получаем

единичный бит. Если хотя бы один из двух сопоставляемых битов равен

нулю, на выходе получаем нуль

|

Оператор поразрядного «или». Сопоставляются соответствующие биты

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

получаем нуль

^

Оператор поразрядного «исключающего или». Сопоставляются соответ-

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

получаем единицу. Если сопоставляемые биты одинаковы, на выходе по-

лучаем нуль

Базовые типы данных и основные операторы            105

Оператор

Описание

>>

Оператор сдвига вправо. Бинарный оператор для выполнения сдвига

вправо битов в побитовом представлении числа. Результат получается

смещением битов в значении переменной, указанной слева от оператора, на количество битов, указанное справа от оператора. При этом старший

знаковый бит сохраняется

<<

Оператор сдвига влево. Бинарный оператор для выполнения сдвига влево

битов в побитовом представлении числа. Результат получается смещением

битов в значении переменной, указанной слева от оператора, на количе-

ство битов, указанное справа от оператора. Младшие биты заполняются

нулями

~

Оператор «дополнение до единицы». В двоичном представлении числа

нули заменяются единицами, а единицы — нулями

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

ПРИМЕЧАНИЕ Для эффективной работы с побитовыми операторами необходимо

хотя бы примерно представлять, как в двоичном коде представляются

числовые значения и как эти значения обрабатываются. Здесь мы

приводим краткую справку по этому поводу.

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

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

записи мы можем легко записать любое, даже самое мало вообрази-

мое число. Причина кроется в достаточно универсальном алгоритме

записи чисел. Причем этот алгоритм касается не только десятичной

системы счисления.

Для обозначения нескольких начальных чисел (начиная с нуля, то есть

0, 1, 2 и так далее) вводятся специальные обозначения — цифры.

Количество цифр определяет систему счисления. В десятичной си-

стеме счисления используют десять цифр, в восьмеричной системе

счисления используют восемь цифр, в двоичной системе счисления

используют две цифры, а в шестнадцатеричной системе — шестнад-

цать (десять цифр и еще шесть букв, которые играют роль «недо-

стающих» цифр). Числа, для которых нет специальных цифр, запи-

сываются с помощью позиционного представления — то есть в виде

последовательности цифр. А именно, любое (неотрицательное) число

записывается в виде последовательности цифр  a a 1...

n n

1

a a

-

0 , где че-

рез  ak  ( k = 0,1,..., n ) обозначены цифры, используемые в системе

счисления. Если речь идет о десятичной системе счисления, то пара-

метры  ak  могут принимать значения от 0 до 9 включительно. Значение

106

Глава 3. Основы синтаксиса языка C#

числа при этом находится по формуле

n

k

anan 1...

-

1

a a 0 = å a 10 .

k=0 k

Для двоичной системы (которая нас в данном случае интересует боль-

ше всего) параметры  ak  могут принимать всего два значения: 0 и 1.

n

Значение числа вычисляется по формуле

k

anan 1...

-

1

a a 0 = å a 2 .

k=0 k

Если бы речь шла о шестнадцатеричной системе счисления, то пара-

метры  ak  принимали бы значения от 0 до 9 и еще шесть букв A, B, C, D, E и F (обозначают числа от 10 до 15 соответственно). Значение

n

числа вычисляется по формуле

k

anan 1...

-

1

a a 0 = å a 16 .

k=0 k

Как отмечалось выше, побитовые операторы оперируют на уровне

двоичного  кода  числа.  В  этом  представлении  число  является  по-

следовательностью нулей и единиц. Сколько этих нулей и единиц

(в  совокупности)  определяется  количеством  бит,  отводимых  для

записи числа. Например, значения типа int запоминаются в виде

последовательности из 32 нулей и единиц. Скажем, число 5 в двоич-

ном представлении типа int имеет вид 00...0101 (всего 32 цифры).

Старшие  нулевые  биты,  как  правило,  не  упоминают,  поэтому  про

число  5  обычно  говорят,  что  в  двоичном  представлении  это  101

(поскольку

0

1

2

1 × 2 + 0 × 2 + 1 × 2 = 5 ).  Однако здесь  появляется

совершенно  неожиданная  проблема.  А  именно,  как  представлять

отрицательные  числа?  Если  бы  мы  записывали  двоичный  код  на

бумаге, то особых проблем не было бы — достаточно перед числом

дописать знак «минус». Но компьютер не знает, что такое «минус».

Он понимает только «0» и «1». Поэтому для записи отрицательных

чисел  используют  военную  хитрость,  которая  у  интеллигентных

людей  проходит  под  кодовым  названием  «дополнение  до  нуля».

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

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

примере поиска двоичного кода для числа -5. Сразу же зададимся

вопросом: что такое число -5? Ответ может быть такой: это число, которое в сумме с числом 5 дает значение 0. Именно от этого посыла

и  будем  отталкиваться.  Решаем  задачу  «от  обратного».  Для  этого

рассмотрим код, который получается в результате инвертирования

бинарного кода числа 5: когда нули заменяются на единицы, а еди-

ницы заменяются на нули. Кстати, соответствующую операцию можно

проделать с помощью побитового оператора ~. Несложно догадаться, что  результатом  выражения  ~5  является  код  11...1010  (всего  32

позиции). Если мы сложим значение 5 и значение ~5, получим код

из всех единиц, то есть значением выражения 5+~5 будет 11...1111

(32 единицы). Добавим к полученному значению число 1. Получим

значение 100..0000 — то есть единица в старшем разряде и еще 32

нуля. Но компьютер в нашем случае запоминает только 32 позиции, поэтому старший единичный бит теряется. А что остается? А остается

32 нуля. Эти 32 нуля на самом деле не что иное, как самый обычный

ноль. Таким образом, для компьютера значение ~5+1 все равно что

число  -5  (с  точки  зрения  конечного  результата).  Несложно  дога-

даться, что это правило остается справедливым и в общем случае:

Базовые типы данных и основные операторы            107

для получения отрицательного числа –число, берем положительное

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

и наоборот) и к полученному коду добавляем единицу.

Чтобы узнать, какое отрицательное число представлено двоичным

кодом, поступаем следующим образом. Инвертируем бинарный код

(получим код положительного числа) и вычисляем десятичное зна-

чение. К этому десятичному значению прибавляем единицу и затем

дописываем знак «минус». Это и есть результат.

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

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

старший бит и называется знаковым битом или битом знака.

Те операторы, что рассматривались выше, были либо бинарными, либо уна-

рными — по количеству операндов (один и два соответственно). Но есть один

оператор, у которого аж три операнда. Поэтому оператор так и называют —

тернарный. Вместе с тем это целая конструкция, которая представляет со-

бой условный оператор, результат которого зависит от некоторого условия.

Синтаксис вызова оператора следующий: условие?выражение1:выражение2.

Результат проверяется так: вычисляется значение условия. Это логиче-

ское значение. Если оно true, в качестве значения тернарного оператора

возвращается значение выражения после вопросительного знака (вы ра же-

ние1). Если оно false, возвращается значение выражения после двоеточия

(выражение2). В принципе, компактно и удобно.

Несколько слов скажем еще об операторе присваивания. Мы уже знаем, что в качестве такового используется знак равенства =. Оператор бинар-

ный. Переменной, указанной слева от оператора присваивания, присваива-

ется значение выражения, указанного справа от оператора присваивания.

В этом нет ничего необычного. Удивить может то, что оператор присваи-

вания возвращает результат. Это означает, что в одном выражении может

быть несколько операторов присваивания: блок с присваиванием перемен-

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

выражение. В этом смысле вполне законной, например, является такая по-

следовательность команд:

int x,y,z;

x=(y=20)+(z=10);

В результате переменная x получает значение 30, переменная y получает

значение 20, а переменная z получает значение 10. Вместе с тем подобно-

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

несколько простых.

108

Глава 3. Основы синтаксиса языка C#

ПРИМЕЧАНИЕ В C# есть такая удивительная штука, как перегрузка операторов.

Благодаря перегрузке операторов действие операторов (не всех, но

многих) «доопределяется» для случая, если операндами являются

объекты пользовательских классов. И хотя для базовых типов и би-

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

служит  одним  из  решающих  аргументов  для  выбора  языка  C#  как

средства программирования. Справедливости ради следует отметить, что в эффектных механизмах в C# недостатка нет.

Основные управляющие инструкции

Мы никогда ничего не запрещаем.

Мы только советуем.

Из к/ф «Забытая мелодия для флейты»

К управляющим инструкциям мы относим всевозможные условные опера-

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

ПРИМЕЧАНИЕ Для знатоков языков программирования C++ и Java сразу отметим, что  различие  управляющих  инструкций  в  языке  C#  по  сравнению

с означенными языками минимально. Хотя некоторые различия все

же есть.

Начнем с условного оператора if(). Этот оператор позволяет создавать

точки ветвления: в зависимости от того, истинно или нет некоторое усло-

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

синтаксис:

if(условие){

// команды — если условие истинно

}

else{

// команды — если условие ложно

}

Выполнение оператора начинается с проверки условия — выражения, ко-

торое в качестве результата возвращает логическое значение (то есть зна-

чение true или значение false). Условие указывается в круглых скобках

после ключевого слова if. Если условие истинно, выполняются команды

Основные управляющие инструкции           109

в фигурных скобках после if-конструкции. На случай, если условие лож-

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

стрирована структурной диаграммой на рис. 3.1.

Рис. 3.1.  Схема работы условного оператора

После завершения выполнения условного оператора управление передает-

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

в C# достаточно демократичный. Например, можно использовать систему

вложенных условных операторов — когда в теле условного оператора вы-

зывается еще один условный оператор, и т. д. Существует также упрощен-

ная форма условного оператора, в которой нет else-блока. Общий синтак-

сис упрощенной формы условного оператора следующий:

if(условие){

// команды — если условие истинно

}

Общая схема выполнения упрощенной формы условного оператора про-

иллюстрирована в структурной диаграмме на рис. 3.2.

Как и в полной версии условного оператора, все начинается с проверки

условия, указанного в скобках после ключевого слова if. Если значение

условия равно true, выполняется блок команд в фигурных скобках. Если

значение условия равно false — ничего не выполняется. Управление сразу

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

110

Глава 3. Основы синтаксиса языка C#

Рис. 3.2.  Схема работы упрощенной формы (без else-блока) условного оператора

ПРИМЕЧАНИЕ Если блок команд в условном операторе состоит всего из одной

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

Вместе  с  тем  наличие  фигурных  скобок  повышает  читабельность

кода и снижает риск ошибки. Поэтому правила хорошего тона под-

разумевают наличие фигурных скобок везде, где это уместно, а не

только там, где это необходимо.

Еще один оператор, который нередко относят к группе условных операто-

ров, — оператор switch(). Основное рабочее название этого оператора —

оператор выбора. Ниже приведен синтаксис вызова этого оператора: switch(выражение){

case значение_1:

// команды — если выражение равно значению_1

break;

case значение_2:

// команды — если выражение равно значению_2

break;

...

case значение_N:

// команды — если выражение равно значению_N

break;

default:

// команды — если совпадение не найдено

break;

}

Основные управляющие инструкции           111

Так все выглядит в кодах. На рис. 3.3 показана схема выполнения операто-

ра выбора в картинках.

Рис. 3.3.  Схема выполнения оператора

выбора с default-блоком

В словах последовательность выполнения оператора выбора может быть

описана следующим образом. После ключевого слова switch в круглых

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

жения. Если совпадение найдено, выполняются команды соответствующе-

го case-блока. Последний default-блок является блоком по умолчанию

команды этого блока выполняются в случае, если ни в одном case-блоке

совпадение не найдено. Блок по умолчанию не является обязательным.

Как выполняется оператор выбора без default-блока, иллюстрирует диа-

грамма на рис. 3.4.

Если в операторе выбора блока по умолчанию нет, то при отсутствии со-

впадений управление передается следующему после оператора выбора

оператору.

112

Глава 3. Основы синтаксиса языка C#

Рис. 3.4.  Схема выполнения оператора выбора

без default-блока

Каждый блок оператора выбора, в том числе и блок по умолчанию, обычно заканчивается инструкцией break. Эта инструкция имеет до-

статочно  универсальное назначение  и  останавливает выполнение

операторов цикла и, в частности, оператора выбора.

В некотором отношении оператор выбора объединяет в себе свойства как

условного оператора, так и оператора цикла. В C# есть несколько опера-

торов цикла и еще одна замечательная инструкция goto, которая в извест-

ном смысле стоит целого оператора. И сейчас как раз наступило время для

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

струкциями.

Достаточно простой, с точки зрения синтаксиса и логики выполнения, опе-

ратор цикла while(). В круглых скобках после ключевого слова while ука-

зывается выражение, возвращающее значение логического типа. По нашей

доброй традиции такие выражения мы просто и лаконично называем усло-

виями. Так вот, если выражение (условие) возвращает значение true, вы-

полняется блок команд в фигурных скобах сразу после while-инструкции.

После этого снова проверяется условие. Если получаем значение true, блок команд выполняется снова. Так продолжается до тех пор, пока зна-

чение условия не станет равным false. Если это произошло, работа опера-

тора цикла while() заканчивается и управление передается следующему

Основные управляющие инструкции           113

оператору после оператора цикла. Последовательность действий проил-

люстрирована диаграммой на рис. 3.5.

Рис. 3.5.  Схема выполнения оператора цикла while() Синтаксис вызова оператора цикла while() такой:

while(условие){

// команды — если условие истинно

}

У оператора while() есть брат-близнец. Это оператор do­while(). Обра-

тимся к синтаксису этого оператора:

do{

// команды — если условие истинно

}while(условие);

От оператора while() оператор do­while() отличается тем, что сначала

выполняется блок команд в теле оператора (в фигурных скобках между

ключевыми словами do и while) и только после этого проверяется условие.

Если условие истинно, снова выполняются команды в теле оператора цик-

ла, и так до достижения значения false в условии.

ПРИМЕЧАНИЕ Таким образом, если мы используем оператор цикла do-while(), ко-

манды тела цикла будут выполнены по крайней мере один раз, чего

нельзя сказать об операторе while().

Последовательность выполнения оператора цикла do­while() отмечена

в структурной диаграмме на рис. 3.6.

114

Глава 3. Основы синтаксиса языка C#

Рис. 3.6.  Схема выполнения оператора цикла do-while() Но на этом операторы цикла не заканчиваются. На сцену выходит един-

ственный и неповторимый в своей непредсказуемости оператор цикла

for(). По сравнению со своими предшественниками, у этого оператора до-

статочно запутанный, хотя и стильный синтаксис:

for(инициализация;условие;изменение){

// команды — если условие истинно

}

Хотя все основное действо и разворачивается обычно в теле оператора цик-

ла, принципиальное значение имеет структура (или «начинка») for-блока.

Вот некоторые правила, о которых следует помнить при работе с операто-

ром цикла for().

 В круглых скобках после ключевого слова for размещается три блока

команд. Каждый блок разделяется точкой с запятой.

 Блоки могут быть пустыми. Если блок состоит из нескольких команд, такие команды внутри блока разделяются запятыми.

 Непосредственно команды тела оператора цикла размещаются в фигур-

ных скобках после for-блока (имеется в виду ключевое слово for и три

блока команд в круглых скобках). Если тело цикла состоит из одной

команды, фигурные скобки можно не использовать.

 В начале выполнения оператора цикла выполняются команды первого

блока. Этот блок обычно называется блоком инициализации и выпол-

няется только один раз.

 После выполнения первого блока (блока инициализации) проверя-

ется условие во втором блоке. Этот блок называют блоком условия.

Основные управляющие инструкции           115

Условие — выражение логического типа. Если условие равно true, выполняются команды из тела оператора цикла (команды в фигурных

скобках). Если условие равно false, работа оператора цикла завершает-

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

(значение true).

 После выполнения команд тела оператора цикла выполняются команды

в третьем блоке for-инструкции. Третий блок обычно называют блоком

изменения (или инкремента/декремента), поскольку обычно в этом блоке

размещают команды для изменения значения индексной переменной.

 После выполнения команд третьего блока проверяется условие. Если

условие истинно (значение true), выполняются команды тела оператора

цикла. Если условие ложно (значение false), работа оператора цикла

завершается.

Схема выполнения оператора цикла for() проиллюстрирована на рис. 3.7.

Для удобства и разрешения неоднозначных ситуаций линии, определяю-

щие последовательность выполнения блоков, экипированы стрелками.

Рис. 3.7.  Схема выполнения оператора цикла for()

Даже из изложенного выше становится совершенно очевидно, что опера-

тор цикла for() допускает огромное количество способов вызова. Причем

многие из них имеют не только смысл, но и оправданы с практической

точки зрения. Некоторые из таких вариантов мы рассмотрим чуть позже.

А сейчас несколько слов хочется посвятить многострадальной инструкции

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

Инструкция goto позволяет передавать управление определенному месту

в программе. Это место определяется с помощью метки. Синтаксис вызова

116

Глава 3. Основы синтаксиса языка C#

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

любой допустимый синтаксисом C# (незарезервированный) идентифика-

тор. Метку не нужно как-то описывать, она просто размещается в коде. По-

сле метки ставится двоеточие. В принципе это все, что касается инструкции

goto и меток. Почему мы рассматриваем эту инструкцию здесь? Потому

что с помощью простой метки и не менее простой инструкции goto можно, кроме прочего, организовать оператор цикла. Правда, придется привлечь

еще и условный оператор, но это уже мелочи.

На этом краткий теоретический обзор управляющих инструкций языка

программирования C# можно заканчивать. Памятуя о том, что практика

есть лучший критерий истины, реализуем наши познания в программных

кодах.

В C# есть еще один оператор цикла foreach(), который в основном ис-

пользуется с массивами (да и то не со всеми). Поэтому до конца завесу

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

Кроме  того,  не  следует  сбрасывать  со  счетов  систему  обработки

исключительных  ситуаций  (блок  try-catch),  с  которой  мы  неожи-

данно столкнулись в первой главе. Хотя напрямую к управляющим

инструкциям эта алхимия не относится, умело используя систему от-

слеживания (и генерирования!) ошибок можно добиваться воистину

удивительных эффектов — куда там управляющим инструкциям!

Как иллюстрацию в использовании наших новых знакомых (имеются

в виду управляющие инструкции) рассмотрим пример, приведенный в ли-

стинге 3.1. Программа достаточно простая:

Листинг 3.1.  Знакомство с управляющими инструкциями

using System;

// Класс с методами для вычисления

// суммы натуральных чисел:

class Summator{

// Поле определяет количество слагаемых:

int n;

// Конструктор класса (с одним аргументом):

public Summator(int n){

// Проверка выхода аргумента за

// пределы диапазона от 1 до 100:

if(n>100){ // Проверка условия

// Если аргумент больше 100:

Console.WriteLine("Слишком большое число! Изменено на 100.");

Основные управляющие инструкции           117

this.n=100;

}

else{

if(n<1){ // Проверка условия

// Если аргумент меньше 1:

Console.WriteLine("Слишком маленькое число! Изменено на 1."); this.n=1;

}

else{

// Если аргумент попадает в диапазон

// от 1 до 100:

this.n=n;

Console.WriteLine("Значение "+this.n+" принято.");

}

}

// Отображается сообщение о вычислении суммы:

Console.WriteLine("Вычисление суммы от 1 до "+this.n+".");

}

// Вычисление суммы с помощью оператора while:

int useWhile(){

// Сообщение о том, какой оператор используется:

Console.Write("Используем оператор while. ");

// Индексная переменная и переменная

// для вычисления суммы:

int i=0,s=0;

// Оператор цикла while:

while(i

// Изменение индексной переменной

// (для подсчета циклов):

i++;

// Изменение переменной для подсчета суммы:

s+=i;

}

// Результат метода:

return s;

}

// Вычисление суммы с помощью оператора do-while:

int useDoWhile(){

// Сообщение о том, какой оператор используется:

Console.Write("Используем оператор do-while. ");

// Индексная переменная и переменная

// для вычисления суммы:

int i=0,s=0;

// Оператор цикла do-while:

do{

продолжение

118

Глава 3. Основы синтаксиса языка C#

Листинг 3.1 (продолжение)

// Изменение индексной переменной

// (для подсчета циклов):

i++;

// Изменение переменной для подсчета суммы:

s+=i;

}while(i

// Результат метода:

return s;

}

// Вычисление суммы с помощью оператора for:

int useFor1(){

// Сообщение о том, какой оператор используется:

Console.Write("Используем оператор for (первый вариант). ");

// Индексная переменная и переменная

// для вычисления суммы:

int i,s=0;

// Оператор цикла:

for(i=1;i<=n;i++){

// Изменение переменной для подсчета суммы:

s+=i;

}

// Результат метода:

return s;

}

// Вычисление суммы с помощью оператора for:

int useFor2(){

// Сообщение о том, какой оператор используется:

Console.Write("Используем оператор for (второй вариант). ");

// Индексная переменная и переменная

// для вычисления суммы:

int i=0,s=0;

// Оператор цикла for с двумя пустыми блоками:

for(;i

// Изменение индексной переменной:

i++;

// Изменение переменной для подсчета суммы:

s+=i;

}

// Результат метода:

return s;

}

// Вычисление суммы с помощью оператора for:

int useFor3(){

// Сообщение о том, какой оператор используется:

Console.Write("Используем оператор for (третий вариант). ");

Основные управляющие инструкции           119

// Индексная переменная и переменная

// для вычисления суммы:

int i,s;

// Оператор цикла for с пустым телом:

for(i=1,s=0;i<=n;s+=i++);

// Результат метода:

return s;

}

// Вычисление суммы с помощью оператора for:

int useFor4(){

// Сообщение о том, какой оператор используется:

Console.Write("Используем оператор for (четвертый вариант). ");

// Индексная переменная и переменная

// для вычисления суммы:

int i=1,s=0;

// Оператор цикла for с пустыми блоками:

for(;;){

// Изменение индексной переменной

// и переменной для вычисления суммы:

s+=i++;

// Условный оператор для проверки условия

// выхода из цикла:

if(i>n) break;

}

// Результат метода:

return s;

}

// Вычисление суммы с помощью инструкции goto:

int useGoto(){

// Сообщение о том, какой оператор используется:

Console.Write("Используем инструкцию goto. ");

// Индексная переменная и переменная

// для вычисления суммы:

int i=1,s=0;

// Метка:

start:

// Изменение переменной для вычисления суммы:

s+=i;

// Изменение индексной переменной:

i++;

// Условный оператор для перехода к метке:

if(i<=n) goto start;

// Результат метода:

return s;

}

продолжение

120

Глава 3. Основы синтаксиса языка C#

Листинг 3.1 (продолжение)

// Метод для отображения результата

// вычислений выбранным методом:

public void show(char choice){

// Отображение символьного аргумента метода:

Console.Write(choice+") ");

// Переменная для вычисления суммы:

int res;

// Оператор выбора:

switch(choice){

case 'A':

res=useWhile();

break;

case 'B':

res=useDoWhile();

break;

case 'C':

res=useFor1();

break;

case 'D':

res=useFor2();

break;

case 'E':

res=useFor3();

break;

case 'F':

res=useFor4();

break;

default:

res=useGoto();

break;

}

// Отображаем результат:

Console.WriteLine("Результат: "+res);

}

}

// Класс с главным методом программы:

class SummatorDemo{

// Главный метод программы:

public static void Main(){

// Объектная переменная:

Summator obj;

// Оператор цикла с целочисленной

// индексной переменной:

for(int i=-25;i<160;i+=50){

// Создание нового объекта:

Основные управляющие инструкции           121

obj=new Summator(i);

// Оператор цикла с символьной

// индексной переменной:

for(char s='A';s<'H';s++){

// Отображение результата

// вычислений выбранным методом:

obj.show(s);

}

// Переход к новой строке:

Console.WriteLine();

}

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

Основу нашей программы составляет класс Summator, предназначенный для

решения исключительно банальной задачи — вычисления суммы натураль-

ных чисел. У класса есть закрытое целочисленное поле n, значение которо-

го определяет количество слагаемых в сумме (то есть мы вычисляем сумму

чисел от 1 до n включительно). Значение полю можно присвоить только

при создании объекта, передав присваиваемое полю значение аргументом

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

значений от 1 до 100 включительно. Если конструктору передан аргумент, меньший 1, полю n присваивается единичное значение. Если аргумент кон-

структора больше 100, полю n присваивается значение сто. Для проверки со-

ответствующих условий использованы вложенные условные операторы: сна-

чала проверяется условие, что аргумент конструктора больше 100. Если это

так, выполняются команды Console.WriteLine("Слишком большое число! Из­

менено на 100.") и this.n=100. Если условие не выполнено, запускается

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

меньше 1. Если он действительно меньше 1, выполняются команды Console.

WriteLine("Слишком маленькое число! Изменено на 1.") и this.n=1. Если же

и это второе условие ложно (то есть аргумент не больше 100 и не меньше 1), то

выполняться будут команды this.n=n и Console.WriteLine("Значение "+this.

n+" принято."). После завершения работы условных операторов командой

Console.WriteLine("Вычисление суммы от 1 до "+this.n+".") выводится со-

общение о вычислении суммы натуральных чисел.

Таким образом, при создании объекта поле n получает значение в диапазо-

не от 1 до 100 и в консольное окно выводится сообщение соответствующего

содержания.

Помимо конструктора, у класса Summator множество методов, назначе-

ние которых — вычислить сумму натуральных чисел. Для этого в разных

122

Глава 3. Основы синтаксиса языка C#

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

использовании разных операторов цикла или различных способах их ис-

пользования).

ПРИМЕЧАНИЕ Все эти методы возвращают целочисленный результат — значение

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

метода выводится сообщение о том, какой метод используется для

вычисления результата.

У методов достаточно красноречивые названия. Так, в методе useWhile() сумма вычисляется с помощью оператора while(). В теле метода объявля-

ются две целочисленные переменные: индексная i для подсчета циклов

и переменная s для подсчета суммы (в эту переменную записывается те-

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

с нулевыми начальными значениями, после этого запускается оператор

цикла while(). Проверяемым условием является i

полняется до тех пор, пока индексная переменная меньше значения поля

n объекта. В теле оператора цикла командой i++ на единицу увеличивает-

ся значение индексной переменной, после чего командой s+=i значение

переменной, предназначенной для подсчета суммы, увеличивается на

текущее значение индексной переменной. После завершения оператора

цикла в переменную s записано нужное значение — сумма чисел от 1 до n.

Поэтому командой return s значение этой переменной возвращается как

результат метода.

В методе useDoWhile() для вычисления суммы используется оператор dowhile(). Здесь, собственно, интриги особой нет — практически все так же, как и в предыдущем случае.

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

цикла в доке инициализации индексная переменная получает единичное

значение (команда i=1). Во втором блоке проверяется условие i<=n. Это

означает, что на последнем цикле команда тела цикла s+=i будет выполне-

на при значении n для индексной переменной i. Эта индексная переменная

каждый раз увеличивает свое значение на 1 благодаря команде i++ в тре-

тьем блоке после for-инструкции оператора цикла.

В методе useFor2() оператор цикла for() используется несколько ина-

че. В for-инструкции теперь первый и третий блоки пустые. Команда

инициализации индексной переменной перенесена из первого блока в то

место, где эта индексная переменная объявляется. Команда увеличения

индексной переменной на 1 вынесена из третьего блока в тело оператора

цикла.

Основные управляющие инструкции           123

ПРИМЕЧАНИЕ Поскольку индексная переменная инициализирована с нулевым на-

чальным  значением,  команда  увеличения  индексной  переменной

находится  перед  командой  изменения  переменной  со  значением

суммы чисел. Также учитывая, что в вычислениях в теле цикла зна-

чение индексной переменной увеличено на 1, по сравнению с тем

значением, для которого проверялось условие, нестрогое неравенство

в условии заменено на строгое.

В методе useFor3() оператор цикла for() имеет пустое тело (отсутствуют

команды после for-инструкции). В первом блоке for-инструкции — две

команды (разделенные запятой), которыми инициализируются перемен-

ные i и s. Команды изменения переменных s и i объединены в одну и на-

ходятся в третьем блоке for-инструкции.

ПРИМЕЧАНИЕ Поскольку в команде s+=i++ использована постфиксная форма опе-

ратора инкремента, то сначала значение переменной s увеличивается

на текущее (старое) значение переменной i, и после этого на 1 увели-

чивается значение переменной i. Команда s+=i++ эквивалентна двум

последовательно выполняемым командам, s=s+i и i=i+1.

Наконец, в методе useFor4() встречаем прямо противоположную ситуа-

цию: for-инструкция совершенно не содержит команд, и все три блока пу-

стые. Поскольку здесь мы встречаем пустой второй блок, оператор цикла

является формально (и неформально тоже) бесконечным. Поэтому в теле

оператора цикла предусмотрена возможность его завершения. Для этого

в условном операторе проверяется условие i>n, и если это условие истин-

но, выполняется инструкция break.

На фоне всех этих методов и операторов метод useGoto(), в котором сум-

ма натуральных чисел вычисляется с использованием условного операто-

ра и инструкции безусловного перехода goto, стоит некоторым особняком, хотя на самом деле ничего особенного в этом методе нет. Традиционно ини-

циализируются две переменные — индексная i и переменная s для записи

в эту переменную подсчитываемой суммы. Блок команд, которые мы ранее

помещали в тело цикла, помечен скромной инструкцией start, которая яв-

ляется не чем иным, как меткой. После изменения значений переменных s и i выполняется условный оператор. Если условие i<=n истинно, командой

goto start управление передается тому месту, которое отмечено меткой

start. В результате получается такой импровизированный оператор цикла.

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

том методе show(). У этого метода один символьный аргумент. Буква, пере-

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

124

Глава 3. Основы синтаксиса языка C#

сумма натуральных чисел. Соответствие устанавливается с помощью опера-

тора выбора switch(). Варианты перебираются с помощью прописных букв

латиницы, начиная с 'A' (затем 'B', 'C' и т. д., до буквы 'F' включительно).

В зависимости от переданной аргументом методу show() буквы вызывается

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

не от 'A' до 'F') используется метод, базирующийся на инструкции goto.

Помимо вычисления непосредственно суммы, методом show() в консоли

отображается вычисленное значение (а также буква, которая передавалась

аргументом методу).

В главном методе программы инструкцией Summator obj объявляется объ-

ектная переменная. После этого запускается оператор цикла, в котором

индексная переменная принимает значения -25, 25, 75 и 125. Каждое из

этих значений используется как аргумент конструктора при создании объ-

екта класса Summator. После этого запускается еще один оператор цикла.

Его особенность в том, что индексная переменная имеет тип char. Коман-

да инкремента индексной переменной в этом случае фактически сводится

к смене символьного значения переменной на следующую букву в кодовой

таблице символов. Для отображения результатов вычислений вызывает-

ся метод show() с соответствующим символьным аргументом. На рис. 3.8

Рис. 3.8.  Результат выполнения программы с управляющими инструкциями

Массивы большие и маленькие           125

представлен результат работы программы: в консольном окне отобража-

ется результат вычисления суммы натуральных чисел разными методами

для разного количества слагаемых.

Как и следовало ожидать, вне зависимости от использованного метода, ре-

зультат неизменен.

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

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

случае совершенно неуместна.

Массивы большие и маленькие

— Какая гадость.

— Это не гадость. Это последние

достижения современной науки.

Из к/ф «31 июня»

Представим себе такую ситуацию: нам нужно в программе создать не-

сколько целочисленных переменных. Уже в этом месте становится груст-

но — ведь чего только стоит подобрать каждой переменной имя. Но мы

применим алгоритмический подход. Другими словами, попробуем авто-

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

процедуру объявления переменных. Кульминацией этой простой, а где-то

даже банальной мысли стали массивы — коллекции однотипных перемен-

ных, которые объединены не только общей целью существования, но и об-

щим именем. Переменные, которые входят в массив (составляют массив) называются элементами массива.

Массивы, особенно в C#, могут быть самыми разными. Мы начнем с наи-

более простых вариантов. Итак, сначала рассмотрим одномерные массивы.

В этом случае важны следующие обстоятельства:

 тип элементов массива — нужно знать, сколько памяти отводить под

каждый из элементов массива;

 количество элементов в массиве (размер массива);

 название массива — можно создать массив и без названия, но это скорее

экзотика. Во всяком случае, для того уровня программирования, на ко-

тором мы временно находимся.

126

Глава 3. Основы синтаксиса языка C#

ПРИМЕЧАНИЕ Кстати, с названием массива дела обстоят не так просто, как может

показаться на первый взгляд. Дело в том, что в C# массивы реализу-

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

именем массива, на самом деле будет переменной массива — пере-

менной, в которой записана ссылка на реальный массив. Идея такая

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

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

Мы начнем с последнего пункта, касающегося имени массива. Имя масси-

ва в C# — это имя переменной, которая содержит ссылку на массив. Что-

бы создать массив, мало его просто создать — необходимо еще объявить

переменную, в которую будет записана ссылка на массив (адрес массива).

Поэтому создание массива состоит из двух этапов: объявление переменной

массива и непосредственно создание массива. Как объявить переменную

массива? Достаточно указать тип элементов массива и имя переменной

массива (это имя мы будем отождествлять с именем массива). Чтобы отли-

чить переменную массива от обычной переменной, при объявлении пере-

менной массива после идентификатора типа элементов массива указывают

пустые квадратные скобки. Например, следующей инструкцией объявля-

ется переменная nums для целочисленного массива:

int[] nums;

Эта переменная может ссылаться на любой целочисленный массив. При

объявлении переменной массива не имеет значения, сколько элементов

в этом массиве — важен только тип этих элементов. Как соотносятся между

собой переменная массива и сам массив, иллюстрирует схема на рис. 3.9.

Рис. 3.9.  Переменная массива и непосредственно массив

В отличие, например, от языка программирования Java, в C# пустые

квадратные  скобки  нельзя  указывать  после  имени  переменной  —

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

Этим  как  бы  подчеркивается,  что  речь  идет  о  переменной  специ-

ального типа.

Массивы большие и маленькие           127

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

элементов массива и, в квадратных скобках, количество элементов в массиве.

Например, вследствие выполнения команды new int[100] создается цело-

численный массив из 100 элементов. Но это еще не все. В качестве результа-

та командой создания массива возвращается ссылка на этот массив. Ссылку

можно записать в переменную массива. Ниже приведены некоторые коман-

ды, которыми создаются два массива (целочисленный и символьный):

// Переменная для целочисленного массива:

int[] nums;

// Создание целочисленного массива:

nums=new int[100];

// Объявление переменной символьного массива и создание массива: char[] syms=new char[20];

Как и в случае с объектами, при создании массива объявление переменной

массива и непосредственное создание массива можно объединять в одну

команду. Обычно так и поступают.

В принципе массивы бывают статическими и динамическими. Прак-

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

компиляции программы. Поэтому в качестве размера статического

массива можно указывать только константу (или числовой литерал —

то есть число). Размер динамического массива может быть определен

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

В C# все массивы динамические.

Для обращения к элементам массива после имени массива в квадратных

скобках указывается индекс элемента в массиве. Индексация массивов

всегда начинается с нуля! Это означает, что первый элемент в массиве име-

ет индекс 0. Индекс последнего элемента в массиве на единицу меньше его

размера — например, для массива из 100 элементов последний, 100-й, эле-

мент будет иметь индекс 99.

Так же просто создаются и многомерные массивы — во всяком случае, ба-

зовый принцип остается неизменным. Создается переменная массива, по-

сле чего с помощью оператора new создается сам массив, а ссылка на этот

массив записывается в переменную массива. Принципиальное отличие от

одномерного массива состоит в том, что

 при объявлении переменной многомерного массива после имени типа

элементов массива в квадратных скобках указываются запятые (коли-

чество запятых — размерность массива минус один);

128

Глава 3. Основы синтаксиса языка C#

 при создании многомерного массива в квадратных скобках указывается

размер (количество элементов) для каждой размерности (в качестве

разделителей используются запятые).

ПРИМЕЧАНИЕ Размерность массива определяется количеством индексов, которые

необходимо  указать  для  однозначной  идентификации  элемента

в массиве. В языке C# все индексы выделяются одной парой ква-

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

ля — в отличие от таких языков программирования, как C++ и Java, в которых для каждого индекса используется своя пара квадратных

скобок.

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

массивы. Ниже приведены примеры создания двумерного и трехмерного

массивов:

// Переменная для двумерного целочисленного массива:

int[,] nums;

// Создание двумерного целочисленного массива:

nums=new int[10,20];

// Объявление переменной трехмерного символьного массива

// и создание массива:

char[,,] syms=new char[5,10,15];

В листинге 3.2 приведен пример простенького программного кода, в ко-

тором создаются два массива: один — числовой одномерный массив, ко-

торый заполняется числами Фибоначчи, а второй — двумерный массив, заполняется случайным образом буквами. Оба массива являются полями

класса.

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

Листинг 3.2.  Знакомство с массивами

using System;

// Класс с полями для массивов:

class MyArray{

// Поле — переменная одномерного

// числового массива:

int[] fibonacci;

// Поле — переменная двумерного

// символьного массива:

Массивы большие и маленькие           129

char[,] symbols;

// Конструктор класса:

public MyArray(int n){

int i,j;

// Создание объекта для генерирования

// случайных чисел:

Random rnd=new Random();

// Создание одномерного целочисленного

// массива:

fibonacci=new int[n];

// Создание символьного двумерного массива:

symbols=new char[n-2,n+2];

// Начальные числа в последовательности

// Фибоначчи:

fibonacci[0]=1;

fibonacci[1]=1;

// Заполнение целочисленного массива

// числами Фибоначчи:

for(i=2;i

fibonacci[i]=fibonacci[i-1]+fibonacci[i-2];

}

// Заполнение двумерного массива

// случайными буквами:

for(i=0;i

for(j=0;j

// Команда с явным преобразованием типа:

symbols[i,j]=(char)('A'+rnd.Next(n));

}

}

}

// Метод для отображения числового массива:

void showNums(){

Console.WriteLine("Числа Фибоначчи:");

for(int i=0;i

Console.Write(fibonacci[i]+" ");

}

Console.WriteLine();

}

// Метод для отображения символьного массива:

void showSyms(){

Console.WriteLine("Случайные буквы:");

for(int i=0;i

for(int j=0;j

Console.Write(symbols[i,j]+" ");

}

продолжение

130

Глава 3. Основы синтаксиса языка C#

Листинг 3.2 (продолжение)

Console.WriteLine();

}

}

// Открытый метод для отображения массивов

// (числового и символьного):

public void show(){

showNums();

Console.WriteLine();

showSyms();

}

}

class ArrayDemo{

public static void Main(){

// Создание объекта:

MyArray obj=new MyArray(10);

// Отображение массивов — полей объекта:

obj.show();

Console.ReadLine();

}

}

В принципе код достаточно простой, но все же есть несколько моментов, на которые стоит обратить внимание. В первую очередь это, конечно, спо-

соб создания массивов-полей класса. Здесь интрига небольшая — соответ-

ствующие поля являются переменными массива (соответствующего типа).

Например, поле для хранения целочисленного одномерного массива с чис-

лами Фибоначчи объявляется как int[] fibonacci — классическая пере-

менная одномерного массива. Поле char[,] symbols представляет собой

переменную двумерного символьного массива.

Следует понимать, что такое объявление полей-массивов на самом деле

не означает создания массивов. Это всего лишь переменные массивов. По

умолчанию значениями этих переменных являются пустые ссылки (или

null-ссылки). Массивы нужно как-то создать. Мы будем создавать масси-

вы в конструкторе.

Конструктор класса имеет один целочисленный аргумент. Массивы соз-

даются командами fibonacci=new int[n] и symbols=new char[n-2,n+2]

(здесь n — аргумент конструктора). После этого созданные массивы за-

полняются значениями. С числовым массивом все достаточно просто: пер-

вые два элемента получают единичные значения (команды fibonacci[0]=1

и fibonacci[1]=1). Для заполнения прочих элементов массива вызывается

оператор цикла, в котором каждое новое значение вычисляется на основе

двух предыдущих (команда fibonacci[i]=fibonacci[i-1]+fibonacci[i-2]

в теле оператора цикла).

Массивы большие и маленькие           131

В  C#  для  определения  количестве  элементов  массива  используют

свойство Length. Свойство вызывается из переменной массива. Для

одномерного массива значение этого свойства совпадает с разме-

ром  массива.  Например,  инструкцией  fibonacci.Length  в  качестве

значения  возвращается  размер  массива  fibonacci.  Для  многомер-

ного массива это общее количество элементов. Чтобы определить

размер  массива  по  определенной размерности, используют  функ-

цию  GetLength(индекс).  Здесь  в  качестве  аргумента  указывается

индекс размерности массива (индексация начинается с нуля). Так, инструкция symbols.GetLength(0) дает количество элементов массива

symbols по первой размерности (размер массива по первому индек-

су), а инструкцией symbols.GetLength(1) возвращается количество

элементов массива symbols по второй размерности (размер массива

по второму индексу).

Поскольку мы планируем заполнять символьный массив случайными бук-

вами, нам нужно создать нечто случайное. Мы, ничтоже сумняшеся, ко-

мандой Random rnd=new Random() создаем объект rnd библиотечного класса

Random, предназначенного для работы со случайными числами. Как след-

ствие, у объекта rnd имеется, кроме прочего, метод Next(), который позво-

ляет генерировать случайные числа. Инструкцией rnd.Next(n) генериру-

ется случайное целое число в диапазоне от 0 до n­1 (n — аргумент метода

Next()). Эта инструкция составляет основу команды symbols[i,j]=(char) ('A'+rnd.Next(n)), которой генерируется случайная буква и присваивается

в качестве значения элементу символьного массива. Формально инструк-

ция 'A'+rnd.Next(n) означает, что к символьной переменной 'A' добавля-

ется некоторое целое число. Сама по себе такая команда является оши-

бочной. Но если перед командой добавить инструкцию (char), все будет

нормально — получим букву. Дело в том, что инструкция вида (тип)(выра­

жение) является командой явного приведения типа. В результате ее выпол-

нения значение выражения приводится к указанному типу. В нашем случае

выражение (char)('A'+rnd.Next(n)) вычисляется так: к коду символа 'A'

добавляется значение rnd.Next(n), и полученный числовой результат при-

водится к символьному типу — полученное число является кодом символа

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

в диапазоне от 'A' до 'J'.

Ранее для символьной переменной мы использовали команду инкре-

мента. При этом ошибки не возникало. Причина в том, что команда

вида x++ для переменной типа char фактически эквивалентна команде

вида x=(char)(x+1).

132

Глава 3. Основы синтаксиса языка C#

В классе есть закрытый метод showNums() для отображения содержимого

числового массива, а также закрытый метод showSyms() для отображения

элементов символьного массива. Оба этих метода последовательно вы-

зываются в открытом методе show(). Именно этот метод мы используем

для отображения содержимого массивов-полей объекта obj класса MyArray в главном методе в классе ArrayDemo. Результат (возможный) выполнения

программы представлен на рис. 3.10.

Рис. 3.10.  Результат выполнения программы с классом, у которого есть поля-массивы

От запуска к запуску результат может меняться, поскольку буквы в дву-

мерном массиве случайные.

Выше мы заполняли массивы с помощью операторов цикла. Но это воз-

можно только в том случае, если значения элементов подчиняются неко-

торой логике. А логика в нашем деле не всегда гарантирована. Поэтому ак-

туальна задача быстрой и простой инициализации массива. Такая метода

есть. Базируется она на том, что при объявлении массива для него указы-

вается список (в фигурных скобках) значений элементов. Примеры при-

ведены ниже:

// Массив из пяти элементов:

int[] nums={1,2,3,4,5};

// Массив из пяти элементов (размер явно не указан):

int[] nums=new int[]{1,2,3,4,5};

// Массив из пяти элементов (явно указан размер):

int[] nums=new int[5]{1,2,3,4,5};

// Двумерный символьный массив (размерами 2 на 3):

char[,] syms={{'A','B','C'},{'D','E','F'}};

// Двумерный символьный массив

// (размерами 2 на 3 — размер явно не указан):

char[,] syms=new char[,]{{'A','B','C'},{'D','E','F'}};

// Двумерный символьный массив

// (размерами 2 на 3 — явно указан размер):

char[,] syms=new char[2,3]{{'A','B','C'},{'D','E','F'}};

Массивы большие и маленькие           133

Если при инициализации массива размер явно не указан, он определяется

автоматически по количеству элементов и способу их группировки (для

многомерных массивов).

Как уже отмечалось, при работе с массивами нередко используется опера-

тор цикла foreach(). Хотя он имеет ограниченную область применимости, в некоторых случаях оператор бывает достаточно полезным. Работу это-

го оператора мы рассмотрим на очень простом примере, представленном

в листинге 3.3.

Листинг 3.3.  Оператор цикла foreach()

using System;

class ForeachDemo{

// Главный метод программы:

public static void Main(){

// Двумерный символьный массив размерами

// 2 (строки) на 3 (столбца):

char[,] symbs={{'A','B','C'},{'D','E','F'}};

// Оператор цикла foreach() — перебираются

// все элементы массива:

foreach(char s in symbs){

// Выводится значение элемента массива:

Console.Write(s+" ");

}

// Переход к новой строке:

Console.WriteLine();

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

В результате выполнения этой программы получаем в консольном окне со-

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

в одну строку, как показано на рис. 3.11.

Рис. 3.11.  Результат выполнения программы с оператором цикла foreach() Инструкция foreach(char s in symbs) означает, что в процессе выполне-

ния оператора цикла локальная переменная s типа char последовательно

перебирает элементы массива symbs. Другими словами, на каждом цикле

134

Глава 3. Основы синтаксиса языка C#

значение переменной s соответствует очередному элементу symbs. И хотя

такой способ обработки массива может показаться удобным, на практике

он не всегда приемлем.

Массивы экзотические и не очень

— Это безрассудство! Тебя могли увидеть!

— Ничего страшного — сочтут

за обыкновенное привидение.

Из к/ф «Тот самый Мюнхгаузен»

До этого мы ограничивались рассмотрением массивов, элементами кото-

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

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

разделе нам будет угодно, чтобы роль элементов на себя примерили объ-

ектные переменные, а также переменные массива. Другими словами, мы

познакомимся с тем, как создавать массивы из объектов, а также массивы

из массивов. Хотя, если принять к сведению, что в C# массивы реализу-

ются по тому же принципу, что и объекты, несложно догадаться, что эти

две задачи на самом деле являются одной задачей — не очень сложной, но

где-то очень экзотической. Начнем с объектов. Обратимся к листингу 3.4, в котором представлен простенький пример того, как объекты можно орга-

низовать в виде одномерного массива.

Листинг 3.4.  Массив объектов

using System;

// Класс для реализации комплексных чисел:

class CNum{

// Действительная часть комплексного числа:

public double Re;

// Мнимая часть комплексного числа:

public double Im;

// Конструктор класса с двумя аргументами:

public CNum(double x,double y){

Re=x;

Im=y;

}

// Метод для отображения параметров числа:

public void show(){

Console.WriteLine("Re="+Re+" и Im="+Im);

}

}

Массивы экзотические и не очень           135

class CNumDemo{

// Главный метод программы:

public static void Main(){

// Размер массива:

int n=9;

// Модуль комплексного числа:

double r=10;

// Локальные переменные:

double x,y;

// Создание массива из объектных переменных:

CNum[] nums=new CNum[n];

// Заполнение массива:

for(int i=0;i

x=r*Math.Cos(2*Math.PI*i/n); // Действительная часть

y=r*Math.Sin(2*Math.PI*i/n); // Мнимая часть

nums[i]=new CNum(x,y); // Создание нового объекта

Console.Write(i+1+"-е число: "); // Отображение текста

nums[i].show(); // Отображение параметров числа

}

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

В программе описывается класс CNum, который имеет некоторую аналогию

с классом для описания комплексных чисел. У класса CNum есть два откры-

тых поля, Re и Im, типа double, которые предназначены для записи, соответ-

ственно, действительной и мнимой частей комплексного числа. У класса

есть конструктор с двумя аргументами и метод show() для отображения

в консоли параметров объекта класса (значений полей Re и Im).

ПРИМЕЧАНИЕ Комплексное число вида  x + iy , где мнимая единица  2 i = -1  по

определению, полностью определяется двумя числами: действитель-

ной частью  x  и мнимой частью  y . Как действительная, так и мни-

мая части комплексного числа по определению являются числами

действительными.

Здесь нет ничего интересного. Все интересное происходит в главном ме-

тоде программы в классе CNumDemo. Помимо обычных команд по объявле-

нию и инициализации локальных переменных в главном методе командой

CNum[] nums=new CNum[n] создается массив из объектных переменных клас-

са CNum. Как мы уже знаем, подобного рода команда является объединением

двух команд: инструкцией CNum[] nums объявляется переменная массива

nums. О чем свидетельствует тип CNum для элементов массива? Он свиде-

тельствует о том, что значениями массива nums могут быть переменные

136

Глава 3. Основы синтаксиса языка C#

типа CNums. А переменные типа CNum являются объектными переменными.

Другими словами, значениями элементов массива CNum могут быть ссылки

на объекты класса CNum. После этого небольшого уточнения дальнейшая

логика создания массива объектов проста и очевидна. Так, инструкцией

new CNum[n] создается массив из n элементов. Ссылка на массив хранит-

ся в переменной nums. В операторе цикла командой nums[i]=new CNum(x,y) в i-й элемент массива записывается ссылка на объект, который создается

командой new CNum(x,y). После этого элемент массива nums[i] ссылается

на объект класса CNum. Из этого объекта можно вызвать метод show(), что

мы и делаем, когда используем в операторе цикла команду nums[i].show().

Результат выполнения программы показан на рис. 3.12.

Рис. 3.12.  Результат выполнения программы с массивом из объектов

В  программном  коде  мы  использовали  некоторые  математические

функции (синус и косинус), а также константу для числа π. Методы

Cos() и Sin() (равно как и константа PI) являются статическими и вы-

зываются из библиотечного класса Math.

Практически точно так же создается массив из массивов, лишь с поправ-

кой на тип элементов — теперь это не объектные переменные, а перемен-

ные массива. Для конкретики рассмотрим создание массива, элементами

которого являются целочисленные массивы (точнее, переменные целочис-

ленных массивов). Переменная целочисленного массива — это перемен-

ная, объявленная с типом int[]. Чтобы создать массив из таких перемен-

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

Ее тип — это тип int[] плюс пустые квадратные скобки []. Получается

int[][]. Дальше рассмотрим программный код в листинге 3.5.

Листинг 3.5.  Массив из массивов

using System;

class BinomDemo{

// Статический метод для отображения элементов целочисленного

//массива:

static void show(int[] m){ // Массив- аргумент метода

Массивы экзотические и не очень           137

foreach(int s in m){

Console.Write (s+" "); // Элементы отображаются в ряд

}

Console.WriteLine(); // Переход к новой строке

}

// Главный метод программы:

public static void Main(){

int n=15; // Размер массива

// Создание массива из массивов:

int[][] binom=new int[n][];

// Заполнение массива:

for(int i=0;i

binom[i]=new int[i+1]; // Создаем массив-элемент

binom[i][0]=1; // Первый элемент массива-элемента

// Последний элемент массива-элемента:

binom[i][binom[i].Length-1]=1;

// Заполнение внутренних элементов

// массива-элемента:

for(int k=1;k

// Вычисляем биномиальные коэффициенты:

binom[i][k]=binom[i-1][k-1]+binom[i-1][k];

binom[i][binom[i].Length-k-1]=binom[i][k];

}

// Отображаем массив-элемент:

show(binom[i]);

}

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

ПРИМЕЧАНИЕ С помощью представленной программы мы вычисляем «треугольник

Паскаля» — специальным образом упорядоченный набор биномиаль-

ных коэффициентов. По определению биномиальный коэффициент

k

n !

Cn =

,  где  целочисленный  индекс  k   может  принимать

k !( n - k)!

значения от 0 до  n включительно. Эти коэффициенты обладают не-

которой симметрией, которой мы и воспользуемся при их вычислении.

Так,  легко  убедиться,  что  k

n k

Cn

C -

= n . Кроме того, в вычислениях

нам понадобится соотношение  k

k 1

-

k

Cn = Cn 1 + C

-

n 1

- .  Для  некоторых

биномиальных коэффициентов можно записать явные выражения.

Например,  0

C = 1

n

, а  1

Cn = n.

Что касается треугольника Паскаля, то его можно представить как ряды

биномиальных коэффициентов — каждый ряд соответствует фиксиро-

138

Глава 3. Основы синтаксиса языка C#

ванному индексу  n, начиная с нуля. Каждый такой ряд с биномиальны-

ми коэффициентами мы реализуем в виде числового массива. А сами

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

от обратного — сначала создаем внешний массив, а уже после этого

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

Результат выполнения этой программы представлен на рис. 3.13.

Рис. 3.13.  «Треугольник Паскаля»: результат выполнения программы, в которой создается массив из массивов

Проанализируем программный код, который приводит к столь замечатель-

ным результатам. Начнем с малого.

В программе описан статический метод show(), который не возвращает

результат и у которого объявлен аргумент — целочисленный массив (на

самом деле переменная массива). Код у метода простой и прогнозируе-

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

ются значения элементов массива-аргумента. Нам метод show() еще пона-

добится.

ПРИМЕЧАНИЕ Конструкция вида int[][] binom должна быть более-менее понятна. Мы

объявляем переменную binom, которая является переменной массива

с элементами типа int[]. Инструкция new int[n][] означает, что создает-

ся массив из n элементов, а элементы типа int[]. Немного неожиданным

может показаться то, что размер массива указан в первых квадратных

скобках, а не во-вторых, но таковы уж правила синтаксиса.

Массивы экзотические и не очень           139

В главном методе программы командой int[][] binom=new int[n][] мы

объявляем переменную массива binom, создаем массив и ссылку на массив

присваиваем этой переменной.

Для заполнения элементов массива запускается оператор цикла, в кото-

ром с помощью индексной переменной i перебираются элементы массива

binom. При этом размер массива определяется свойством binom.Length.

Еще раз обращаем внимание читателя на то, что массив binom —

одномерный. Его элементы — переменные массива, которые могут

(и будут) ссылаться на одномерные числовые массивы.

Командой binom[i]=new int[i+1] создаются целочисленные массивы, и ссылки на них записываются в переменные массива, которые являются

элементами массива binom. Размер каждого следующего массива на едини-

цу больше размера предыдущего массива. Таким образом, переменная мас-

сива binom[i] ссылается на целочисленный массив размера i+1.

Командой binom[i][0]=1 начальному элементу внутреннего массива

binom[i] присваивается единичное значение. Такую же процедуру мы про-

делываем с последним элементом массива binom[i], для чего вызываем ко-

манду binom[i][binom[i].Length-1]=1.

Если binom[i] — массив, то binom[i][0] — первый элемент массива

binom[i]. Размер массива binom[i] (количество элементов в массиве) может  быть  вычислен  инструкцией  binom[i].Length.  Тогда  индекс

последнего  элемента  binom[i].Length-1,  а  сам  последний  элемент

массива  возвращается  инструкцией  binom[i][binom[i].Length-1].

Присваивая первому и последнему элементам массива binom[i], мы

вычисляем биномиальные коэффициенты  0

C 1 1

i+ =

.

Заполнение внутренних элементов массива binom[i] осуществляется во

вложенном операторе цикла с индексной переменой k. Начальное значе-

ние этой переменной 1, и за каждый цикл ее значение увеличивается на

единицу до тех пор, пока выполняется условие k

цикла выполняются команды binom[i][k]=binom[i-1][k-1]+binom[i-1][k]

и binom[i][binom[i].Length-k-1]=binom[i][k].

После того как массив binom[i] заполнен элементами, отображаем его со-

держимое с помощью команды show(binom[i]). Здесь мы еще раз исполь-

зуем то обстоятельство, что binom[i] — это одномерный целочисленный

массив.

140

Глава 3. Основы синтаксиса языка C#

Здесь следует учесть, что элемент binom[i][k] соответствует биноми-

альному коэффициенту  i 1

C +

k

. Команда binom[i][k]=binom[i-1][k-1]+

+ binom[i-1][k] является применением правила  k

k 1

-

k

C

i 1

C

+ =

i

+ Ci

для  вычисления  биномиальных  коэффициентов.  Она  применима, если значения массива binom[i-1] уже заполнены. Вызывая команду

binom[i][binom[i].Length-k-1]=binom[i][k], мы применяем на прак-

тике правило  i 1

+ k

-

k

Ci 1 = C

+

i 1

+ . При этом индексная переменная  k  не

превышает значение  i + 1 - k , то есть имеет место соотношение

k £ i + 1 - k , или, учитывая целочисленность индексных перемен-

ных,  k < i + 2 - k . Важно то, что  i + 2  — это общее количество

биномиальных коэффициентов с нижним индексом  i + 1 . Учитывая, что биномиальные коэффициенты с нижним индексом  i + 1  записа-

ны в массив binom[i], их количество вычисляем инструкцией binom[i].

Length.  Отсюда  и  условие  для  индексной  переменной  k

Length-k.

Знакомство с указателями

Ну зачем такие сложности?!

Из к/ф «Приключения Шерлока Холмса

и доктора Ватсона. Собака Баскервилей»

В C# есть достаточно специфичный тип данных — указатели. Значени-

ем переменной-указателя (или просто указателя) является адрес другой

переменной. Другими словами, в качестве значения указателю можно при-

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

ные переменные. Однако, в отличие от объектных переменных, поведение

которых прописано и контролируется, с указателями можно проделывать

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

ПРИМЕЧАНИЕ Указатели в C# — это отголосок языка С++, в котором без них и шагу не

ступить. Правда, в C# указатели намного консервативнее. Например, указатели могут ссылаться только на нессылочные данные — то есть

на объект указатели не ссылаются (но зато могут ссылаться на поля

объекта). Но это все же лучше, чем их полное отсутствие — как, на-

пример, в языке Java.

Знакомство с указателями           141

Мы не планируем массово использовать указатели, поэтому здесь состоит-

ся только краткое знакомство с ними. Для начала выясним, как объявля-

ется указатель. А объявляется он достаточно просто: практически так же, как обычная переменная, только в качестве типа указывается базовый тип

переменной, на которую ссылается указатель, и символ * (звездочка). На-

пример, если мы хотим создать указатель на переменную целочисленно-

го типа, то соответствующее объявление могло бы выглядеть как int* p.

Здесь p — это имя переменной-указателя, символ * есть индикатор того, что это именно указатель, а идентификатор типа int является молчаливым

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

ной переменной типа int. Аналогично, для создания указателя на double-

переменную, используем инструкцию вида double* q, и т. д.

Есть два полезных оператора, которые часто используются при работе

с указателем. С помощью оператора & можно получить адрес перемен-

ной — достаточно указать этот оператор перед именем переменной. Об-

ратную процедуру (узнать, какое значение записано по адресу, который

является значением указателя) позволяет выполнить оператор *. Этот опе-

ратор указывается перед переменной-указателем. Но это еще не все. Если

программа содержит блок с указателями, этот блок кода должен быть по-

мечен специальным ключевым словом unsafe. Нередко это ключевое слово

указывают в заголовке метода, в котором использованы указатели. Более

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

инструкции /unsafe.

Ключевым  словом  unsafe  отмечается  небезопасный  код.  Дело  в

том, что через указатели мы получаем прямой доступ к операциям

с памятью. Исполнительная система не может гарантировать пол-

ную безопасность программного кода с указателями. Корректность

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

ной нашей ответственности. Но это совсем не означает, что код с

указателями какой-то ущербный. Просто нужно реально осознавать

степень риска и степень ответственности. Что касается компиляции

программы с параметром /unsafe, то в среде Visual C# Express необ-

ходимо в меню Проект выбрать команду Свойства, в раскрывшейся

вкладке с именем проекта выбрать раздел Построение и установить

флажок опции Разрешить небезопасный код. Иначе проект не от-

компилируется.

Методы работы с указателями рассмотрим на небольшом примере. Он

приведен в листинге 3.6.

142

Глава 3. Основы синтаксиса языка C#

Листинг 3.6.  Знакомство с указателями

using System;

class PointerDemo{

// Используем атрибут unsafe:

unsafe public static void Main(){

int* p; // Объявляем указатель

int n; // Объявляем обычную переменную

p=&n // Указатель "помнит" адрес переменной n n=100; // Переменной n присвоили значение

// По адресу-значению указателя p

// записываем значение:

*p=200;

Console.WriteLine("n="+n); // Проверяем результат

Console.ReadLine(); // Ожидание нажатия клавиши Enter

}

}

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

ние n=200. Проанализируем, почему происходит именно так. Для этого

разберем поэтапно команды в методе Main() (который, кстати, объявлен

с атрибутом unsafe). Командой int* p объявляется указатель p на целочис-

ленную переменную. Целочисленная переменная объявляется следующей

командой int n. Связь между указателем p и переменной n появляется по-

сле выполнения команды p=&n. В результат адрес, по которому прописа-

на переменная n, записывается в качестве значения в указатель p. Затем

переменной n присваиваем значение 100. Но после выполнения команды

*p=200 переменная n получает значение 200. Почему? Да потому, что *p —

это ссылка на значение, которое прописано по адресу p. А по этому адресу

прописана переменная n. Поэтому значение именно этой переменной ме-

няется.

То, что мы увидели, — это только вершина айсберга. У указателей множе-

ство удивительных свойств. Например:

 Арифметические операции с указателями выполняются по особым пра-

вилам — по правилам адресной арифметики. Например, разность двух

указателей — это целое число, определяющее количество ячеек между

адресами, на которые ссылаются указатели.

 Имя массива является указателем на его первый элемент.

 Указатели можно индексировать — почти как массивы.

 Из указателей можно создавать массив и делать много других удиви-

тельных вещей.

Однако рассмотрение всех этих вопросов не вписывается в наши планы.

Перегрузка

операторов

Что бы мы делали без науки?

Подумать страшно!

Из к/ф «31 июня»

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

в C# можно доопределить так, что, можно будет эти самые операторы при-

менять в отношении объектов, созданных силой воображения пользовате-

ля (или программиста — это как посмотреть). Называется данное действо

перегрузкой операторов. Именно она будет занимать все наши помыслы

вплоть до окончания данной главы. А может и больше — кому как повезет.

Операторные методы и перегрузка

операторов

— Благородная Нинэт, я вам предлагаю маленький заговор.

— А большой нельзя?

— Маленький, но с большими последствиями.

— Что надо делать? Я готова на все.

Из к/ф «31 июня»

Перегрузка операторов — это, если хотите, особая философия, в основе

которой лежит понятие операторного метода. А чтобы понять, что такое

144

Глава 4. Перегрузка операторов

операторный метод, придется задуматься и задаться вопросом, который

может показаться странным: чем оператор отличается от метода? Если

«пройтись по верхам», то ответ будет «всем». Если «копнуть вглубь», то

ответ будет «ничем». А истина, как известно, всегда находится где-то по-

средине между наиболее радикальными вариантами.

Нам, для решения поставленной задачи по перегрузке операторов, удобно

будет думать об этих самых операторах как об особого типа методах. Для

обычного метода (при вызове метода в команде) аргументы указываются

в круглых скобках после имени метода. Для оператора роль аргументов

играют операнды. Специфическое обозначение оператора служит альтер-

нативой имени метода. Поэтому задача перегрузки оператора для какого-

то определенного класса может рассматриваться как определение для этого

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

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

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

Соответствие между операторами и операторными методами устанавлива-

ется просто: имя операторного метода, соответствующего определенному

оператору, получается объединением ключевого слова operator и символа

оператора. Например, операторный метод для оператора сложения + будет

называться operator+. Для оператора умножения * операторный метод на-

зывается operator*, и т. д.

Существует несколько правил, которых необходимо придерживаться при

описании операторных методов в классе.

 Операторные методы описываются с атрибутами public и static.

Операторные методы должны быть открытыми и статическими, что

вполне понятно, поскольку метод должен быть доступен вне класса (от-

крытость метода) и относится он к классу как такому, а не к отдельному

объекту (статичность метода).

 Количество аргументов операторного метода совпадает с количеством

операндов соответствующего оператора: для бинарных операторов у опе-

раторного метода два аргумента, для унарных операторов у операторного

метода один аргумент.

 По крайней мере один из аргументов операторного метода должен быть

объектом класса, в котором этот операторный метод описан.

 Операторный метод должен возвращать результат. Результат оператор-

ного метода — это результат вычисления выражения с перегружаемым

оператором и соответствующими операндами — аргументами оператор-

ного метода.

В листинге 4.1 приведен программный код простенькой программы, в ко-

торой использована перегрузка некоторых операторов — а если быть более

точным, то двух.

Операторные методы и перегрузка операторов           145

ПРИМЕЧАНИЕ Прежде чем приступить к анализу программного кода, имеет смысл

кратко  остановиться  на  общей  идее.  А  идея  в  том,  чтобы  создать

небольшой валютный калькулятор, который позволил бы выполнять

основные  операции  (условные)  с  денежными  суммами  в  разной

валюте.  Для  хранения  валютных  резервов  создаем  специальный

класс,  а  отдельные  транши  будут  реализовываться  через  объекты

этого класса. У класса есть два поля: одно содержит номинальное

значение в иностранной валюте, а еще одно поле содержит значение

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

денежные  суммы  в  разной  валюте.  Понятно,  что  для  проведения

таких  расчетов  необходимо  денежные  суммы  привести  к  общему

знаменателю — выразить в одной и той же валюте. Таким знаме-

нателем в нашем случае будут рубли. Однако еще остается вопрос

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

следующим правилом. Если к долларам прибавляем евро, получаем

доллары.  Если  к  евро  прибавляем  доллары,  получаем  евро.  Если

к  рублям  прибавляем  доллары,  получаем  рубли.  Если  к  долларам

прибавляем рубли, получаем доллары. При этом с рублями мы будем

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

Листинг 4.1.  Перегрузка операторов

using System;

// Класс с перегрузкой операторов:

class Currency{

// Открытые поля класса

public double nominal;

public double rate;

// Конструктор класса:

public Currency(double nominal,double rate){

// Присваивание значений полям:

this.nominal=nominal;

this.rate=rate;

}

// Метод для вычисления стоимости (в рублях):

public double price(){

return nominal*rate;

}

// Метод для отображения параметров объекта:

public void show(){

Console.WriteLine("Номинальная сумма в валюте: "+nominal); Console.WriteLine("Обменный курс (в рублях): "+rate); продолжение

146

Глава 4. Перегрузка операторов

Листинг 4.1 (продолжение)

Console.WriteLine("Стоимость (в рублях): "+price()+"\n");

}

// Перегрузка оператора сложения.

// Операнды — объекты класса:

public static Currency operator+(Currency A,Currency B){

// Объектная переменная:

Currency C;

// Локальные переменные:

double nominal,rate;

// Вычисление значений для создания

// на их основе нового объекта:

rate=A.rate;

nominal=(A.price()+B.price())/rate;

// Создание нового объекта:

C=new Currency(nominal,rate);

// Созданный объект возвращается

// в качестве результата:

return C;

}

// Перегрузка оператора сложения.

// Операнды — объект класса и число:

public static Currency operator+(Currency A,double B){

// Объектная ссылка:

Currency C;

// Локальные переменные:

double nominal,rate;

// Вычисление значений переменных

// для создания на их основе объекта:

rate=A.rate;

nominal=(A.price()+B)/rate;

// Создание объекта:

C=new Currency(nominal,rate);

// Созданный объект возвращается

// в качестве результата:

return C;

}

// Перегрузка оператора присваивания.

// Операнды — число и объект класса:

public static double operator+(double A,Currency B){

// В качестве результата возвращается число:

return A+B.price();

}

// Перегрузка унарного оператора !:

public static double operator!(Currency A){

Операторные методы и перегрузка операторов           147

// Отображается информация об объекте:

A.show();

// Результат операторного метода:

return A.price();

}

}

// Класс с главным методом программы:

class CurrencyDemo{

// Главный метод программы:

public static void Main(){

// Объектные переменные:

Currency Dol, Eur, Money;

// Создание объектов:

Dol=new Currency(100,30);

Eur=new Currency(300,40);

// Сложение объектов:

Money=Dol+Eur;

// Проверяем результат:

Money.show();

// Меняем порядок слагаемых:

Money=Eur+Dol;

// Проверяем результат:

Money.show();

// Складываем объект и число:

Money=Dol+9000;

// Проверяем результат:

Money.show();

// Команда содержит инструкцию

// суммирования числа и объекта:

Console.WriteLine("Сумма в рублях: "+(0+Money)+"\n");

// Проверяем работу перегруженного

// унарного оператора:

Console.WriteLine("Контрольное значение: "+!Money);

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

Все самое интересное описано в классе Currency. У класса два открытых

поля типа double. В поле nominal записывается номинальная сумма в ино-

странной валюте. В поле rate записывается обменный курс — стоимость

единицы иностранной валюты в рублях. У класса конструктор с двумя

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

Также у класса есть ряд полезных методов, среди которых и операторные.

Открытый метод price() вычисляет стоимость валютного объекта в ру-

блях. Чтобы вычислить эту величину достаточно умножить значение поля

148

Глава 4. Перегрузка операторов

nominal на значение поля rate. Именно такое значение метод возвращает

в качестве результата.

Также есть у класса весьма полезный метод show(), которым в консольное

окно выводится вся важная информация об объекте: значения полей и их

произведение.

В классе перегружается, как отмечалось, два оператора — бинарный опе-

ратор сложения + и унарный оператор логического отрицания !. Причем

для оператора сложения в классе предлагается три варианта перегрузки, в зависимости от типа операндов:

 два операнда — объекты класса Currency;

 первый операнд — объект класса Currency, а второй аргумент — числовое

значение типа double;

 первый аргумент — числовое значение типа double, а второй аргумент —

объект класса Currency.

Хотя  мы  привыкли  к  тому,  что  в  математике  операция  сложения

коммутативна (от перестановки слагаемых сумма не меняется), в про-

граммировании изменение порядка операндов может иметь карди-

нальные последствия.

Наше исследование начнем с анализа операторного метода для перегрузки

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

Шапка операторного метода на этот случай выглядит так:

public static Currency operator+(Currency A,Currency B)

Атрибуты public и static традиционны в этом случае, и их мы уже коммен-

тировали. В качестве типа результата указано ключевое слово Currency. Это

означает, что в качестве результата возвращается объект класса Currency.

ПРИМЕЧАНИЕ Если быть более точным, это означает, что результатом метода явля-

ется ссылка на объект класса Currency.

Поскольку перегружается оператор сложения, операторный метод называ-

ется operator+. Аргументы A (первый операнд) и B (второй операнд) — объ-

екты класса Currency. Это означает, что операторный метод будет вызы-

ваться каждый раз, когда мы к объекту класса Currency будем прибавлять

объект класса Currency. Теперь обратимся к программному коду в основ-

ном теле операторного метода.

Поскольку метод в качестве результата возвращает объект, этот объект

в теле метода необходимо создать. Мы начинаем с малого — объявляем

Операторные методы и перегрузка операторов           149

объектную переменную С (команда Currency C). Далее нам предстоит соз-

дать объект. Но предварительно необходимо рассчитать его параметры.

Для этого мы вводим две локальные переменные, nominal и rate (обе типа

double). Эти переменные будут определять значения одноименных полей

создаваемого объекта. Переменной rate значение присваивается коман-

дой rate=A.rate. Значение поля rate объекта-результата будет таким же, как и значение поля rate первого операнда. Значение переменной nominal задается командой nominal=(A.price()+B.price())/rate. Вычисления

простые: «цена в рублях» первого операнда суммируется с «ценой в ру-

блях» второго операнда, а полученное значение делится на курс первой

валюты, который записан в переменную rate. После проведенных нехи-

трых вычислений командой C=new Currency(nominal,rate) смело создаем

новый объект, а командой return C возвращаем его как результат опера-

торного метода.

Практически так же функционирует и версия операторного метода для

оператора сложения в случае, если второй операнд B является числом типа

double. Здесь достаточно учесть, что второй операнд и есть «цена в рублях», поэтому значение переменной nominal определяется командой nominal=(A.

price()+B)/rate.

Намного больше различий в варианте операторного метода, в котором

первый аргумент A есть число, а второй аргумент B — объект. Теперь ре-

зультатом метода является числовое значение типа double, а тело метода

состоит всего из одной команды return A+B.price(). Результат метода вы-

числяется как сумма первого аргумента и «цена в рублях» объекта — вто-

рого операнда.

Практически так же легко перегружается оператор логического отрица-

ния !. Главная его особенность связана с тем, что оператор этот унарный.

Поэтому у операторного метода operator! всего один аргумент, и это объ-

ект A класса Currency. В качестве результата методом возвращается чис-

ловое значение типа double. Тело метода состоит из двух команд. Коман-

дой A.show() отображается информация об объекте-операнде, а командой

return A.price() в качестве результата метода возвращается «рублевая

цена» операнда.

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

ных операторов. Для этого мы создаем три объектные переменные, Dol, Eur и Money, класса Currency. В переменные Dol и Eur записываем ссылки на

объекты, а с переменной Money начинаем эксперименты. Складываем объ-

екты (команды Money=Dol+Eur и Money=Eur+Dol), складываем объект и чис-

ло (команда Money=Dol+9000), а также число и объект (команда 0+Money).

Для проверки параметров объекта Money используется инструкция Money.

show(). Кроме того, в программном коде есть инструкция !Money, в которой

150

Глава 4. Перегрузка операторов

унарный оператор логического отрицания применяется к объекту Money.

Результат выполнения программы показан на рис. 4.1.

Рис. 4.1.  Результат выполнения программы

с перегруженными операторами

Возможно, некоторого пояснения потребует результат выполнения ко-

манды Console.WriteLine("Контрольное значение: "+!Money). Здесь особо

необычного ничего нет. Просто при выводе текстового сообщения вместо

инструкции !Money следует подставить числовое значение — результат

метода Money.price(). Однако перед этим, в соответствии с программным

кодом операторного метода operator!, должна быть выполнена инструк-

ция Money.show(). Эта инструкция выполняется в процессе вычисления

результата выражения !Money, а значит, до того, как будет выведен текст

"Контрольное значение: ".

Стоит также обратить внимание на инструкцию 0+Money. Если бы мы вы-

полнили инструкцию Money+0, получили бы объект такой же, как Money.

А результатом инструкции 0+Money является значение, возвращаемое ме-

тодом Money.price(). Вот насколько важно выдерживать нужный порядок

аргументов/операндов.

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

перегружать оператор присваивания и его сокращенные формы, не

перегружается оператор «точка», и ряд других. Вместе с тем в С# есть

некоторые  трюки,  которые  позволяют  несколько  сгладить  осадок

в душе от таких запретов.

Также стоит обратить внимание на то, что перегрузка оператора для

класса пользователя никак не влияет на способ действия этого опе-

ратора на базовые типы данных и библиотечные классы.

Перегрузка арифметических операторов и операторов приведения типа           151

Перегрузка арифметических

операторов и операторов

приведения типа

— Скажите, доктор Ватсон, вы понимаете

всю важность моего открытия?

— Да, как эксперимент это интересно.

Но какое практическое применение?

— Господи, именно практическое!

Из к/ф «Приключения Шерлока Холмса

и доктора Ватсона. Знакомство»

Рассмотрим еще один пример, в котором перегружается большинство

арифметических операторов. Также есть операторы-сюрпризы. Но, перед

анализом программного кода, по сложившейся традиции — несколько слов

об общей идее, положенной в основу программного кода. Основу кода со-

ставляет класс Vector, предназначенный для работы с такими чудесными

математическими объектами, как векторы.

ПРИМЕЧАНИЕ Стивен Хокинг, один из выдающихся физиков современности, утверж-

дает, что каждая формула в книге вдвое уменьшает количество чита-

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

бегаем к этой вынужденной мере по причине крайней необходимо-

сти — ну на чем-то же надо перегружать арифметические операторы.

Поэтому, осознавая, что многие читатели понятия не имеют, что такое

вектор, приведем краткую векторную справку.

Здесь мы будем вести речь о векторах в трехмерном декартовом про-

странстве. С математической точки зрения такой вектор является на-

бором трех числовых параметров, которые называются координатами

вектора. Обычно векторы обозначаются буквой со стрелкой сверху.

Так, если вектор  a  имеет координаты  1

a ,  a 2  и  a 3 , то этот чудесный

факт отображается записью вида  a = ( 1

a , a 2, a 3) . Для векторов уста-

навливаются некоторые математические операции, которые постули-

руются на уровне операций с координатами векторов. Нас интересуют

следующие операции (векторы  a = ( 1

a , a 2, a 3) и  b = ( 1

b , 2

b , 3

b )):

Сумма векторов: результатом будет являться вектор  c = a + b = ( a

1 + 1

b , a 2 + 2

b , a 3 + 3

b )

c = a + b = ( a 1 + 1

b , a 2 + 2

b , a 3 + 3

b )  — вектор, координаты которого рав-

ны сумме соответствующих координат суммируемых векторов.

152

Глава 4. Перегрузка операторов

Разность векторов: результатом является вектор  c = a - b = ( a

1 - 1

b , a 2 - 2

b , a 3 - 3

b )

c = a - b = ( a 1 - 1

b , a 2 - 2

b , a 3 - 3

b ) — вектор, координаты которого равны

разности соответствующих координат отнимаемых векто ров.

Умножение вектора на число (обозначим его как l ):

λ  результатом

является вектор  c = lλ a = (lλ λ

λ

1

a ,l a 2,l a 3) — вектор, координаты

которого получаются умножением на число каждой координаты

исходного вектора. Деление вектора на число l означает умно-

жение вектора на число 1 l.λ.

Скалярное произведение векторов: результатом является число

 

a × b = 1

a 1

b + a 2 2

b + a 3 3

b  — сумма произведений соответствую-

щих координат векторов.

Модулем  вектора  называется  корень  квадратный  из  скаляр-

 

ного  произведения  вектора  на  самого  себя:

2

2

2

| a |= a × a = a 1 + a 2 + a 3

 

2

2

2

| a |= a × a = a

.

1 + a 2 + a 3

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

 

ними, то есть  a × b |

= a | × | b | ×cos(j ),

(ϕ)  где через j

ϕобозначен угол

между векторами  a  и  b . Это соотношение обычно используют

æ

 

ç a × b

ö

для вычисления угла между векторами: jϕ = arcsin

÷

ç 

 ÷

ç

÷

ç

.

è| a | × | b |÷ø

Единичным вектором

a

e  в направлении вектора  a  называется век-

a

тор  a

e =

a .

| a , то есть вектор  a  делится на свой модуль | |

|

По большому счету вектор — это набор из трех элементов, плюс специфи-

ческие правила обработки этих трех элементов «в комплекте». Нам нужно

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

коде. Мы поступим так: для реализации вектора используем класс с по-

лем — числовым массивом из трех элементов. Для выполнения основных

операций с векторами переопределяем базовые арифметические опера-

торы (и еще два не очень арифметических оператора). Обратимся к про-

граммному коду в листинге 4.2.

Листинг 4.2.  Перегрузка арифметических операторов

using System;

// Класс для реализации векторов:

class Vector{

// Массив - для записи координат вектора:

public double[] coords;

// Конструктор класса (с тремя аргументами):

public Vector(double x,double y,double z){

// Создание массива из трех элементов:

coords=new double[3];

Перегрузка арифметических операторов и операторов приведения типа           153

// Присваивание значений элементам массива:

coords[0]=x;

coords[1]=y;

coords[2]=z;

}

// Перегрузка оператора сложения для

// вычисления суммы векторов:

public static Vector operator+(Vector a,Vector b){

// Создание массива из трех элементов:

double[] x=new double[3];

// Присваивание элементам массива значений:

for(int i=0;i<3;i++){

x[i]=a.coords[i]+b.coords[i]; // Сумма координат векторов

}

// Создание нового объекта с вычисленными

// параметрами (координатами):

Vector res=new Vector(x[0],x[1],x[2]);

// Объект возвращается как результат:

return res;

}

// Перегрузка оператора умножения

// для вычисления скалярного произведения

// векторов:

public static double operator*(Vector a,Vector b){

// Локальная переменная с нулевым

// начальным значением:

double res=0;

// Вычисление суммы попарных произведений

// координат векторов:

for(int i=0;i<3;i++){

res+=a.coords[i]*b.coords[i];

}

// Вычисленное значение возвращается как

// результат:

return res;

}

// Перегрузка оператора умножения

// для вычисления произведения вектора на число:

public static Vector operator*(Vector a,double b){

// Создание массива из трех элементов:

double[] x=new double[3];

// Вычисление значений элементов массива:

for(int i=0;i<3;i++){

x[i]=a.coords[i]*b;

}

продолжение

154

Глава 4. Перегрузка операторов

Листинг 4.2 (продолжение)

// Создание объекта с вычисленными параметрами:

Vector res=new Vector(x[0],x[1],x[2]);

// Объект (ссылка на объект) возвращается

// в качестве результата:

return res;

}

// Перегрузка оператора умножения

// для вычисления произведения числа на вектор:

public static Vector operator*(double b,Vector a){

// То же самое, что произведение

// вектора на число.

// Используем перегруженный оператор

// умножения:

return a*b;

}

// Перегрузка оператора деления

// для вычисления результата деления вектора на

// число:

public static Vector operator/(Vector a,double b){

// Определяем через операцию умножения

// вектора на число:

return a*(1/b);

}

// Перегрузка оператора деления для случая,

// когда операнды - объекты

// класса Vector. В результате вычисляется угол

// (в радианах) между

// соответствующими векторами:

public static double operator/(Vector a,Vector b){

// Локальные переменные для запоминания

// косинуса угла и угла:

double cosinus,phi;

// Вычисление косинуса угла между векторами.

// Используем перегруженный оператор

// произведения и оператор

// явного приведения типа (см. код далее):

cosinus=(a*b)/((double)a*(double)b);

// Вычисление угла:

phi=Math.Acos(cosinus);

// Метод возвращает результат:

return phi;

}

// Перегрузка оператора вычитания для

// вычисления разности двух векторов:

public static Vector operator-(Vector a,Vector b){

// Вычисляем результат с помощью

Перегрузка арифметических операторов и операторов приведения типа           155

// перегруженного оператора

// сложения (двух векторов) и умножения

// (числа на вектор):

return a+(-1)*b;

}

// Перегрузка унарного оператора "минус"

// для вектора:

public static Vector operator­(Vector a){

// Вычисляется как умножение вектора на -1:

return (­1)*a;

}

// Перегрузка оператора инкремента для вектора:

public static Vector operator++(Vector a){

// К вектору добавляется единичный вектор

// того же направления.

// Используем перегруженные операторы деления

// и приведения типа:

a=a+(a/(double)a);

// Возвращаем аргумент как результат:

return a;

}

// Перегрузка оператора декремента для вектора:

public static Vector operator­­(Vector a){

// От вектора отнимается единичный вектор

// того же направления.

// Используем перегруженные операторы

// деления и приведения типа:

a=a­(a/(double)a);

// Возвращаем аргумент как результат:

return a;

}

// Перегрузка оператора явного приведения типа.

// Объект класса Vector приводится к значению

// типа double.

// Результатом является модуль

// соответствующего вектора:

public static explicit operator double(Vector a){

// Результат - корень квадратный из

// скалярного произведения

// вектора на самого себя:

return Math.Sqrt(a*a);

}

// Перегрузка оператора неявного

// приведения типа.

// Объект класса Vector приводится к

// текстовому значению (тип string):

продолжение

156

Глава 4. Перегрузка операторов

Листинг 4.2 (продолжение)

public static implicit operator string(Vector a){

// Результат - текстовая строка с

// координатами вектора:

return "<"+a.coords[0]+";"+a.coords[1]+";"+a.coords[2]+">";

}

}

// Класс с главным методом программы:

class VectorDemo{

// Главный метод программы:

public static void Main(){

// Объектные переменные:

Vector a,b,c;

// Числовые переменные:

double phi,cosinus,expr;

// Первый объект - вектор:

a=new Vector(3,0,4);

// Второй объект - вектор:

b=new Vector(0,6,8);

// Угол между векторами:

phi=a/b;

// Косинус угла между векторами:

cosinus=a*b/((double)a*(double)b);

// Проверка тригонометрического тождества:

expr=Math.Sin(phi)*Math.Sin(phi)+cosinus*cosinus;

// Отображаем результат:

Console.WriteLine("Проверка: sin(phi)^2+cos(phi)^2="+expr);

// Используем оператор инкремента:

a++;

// Используем оператор декремента:

­­b;

// Вычисляем новый вектор:

c=-(a*5-b/2);

// Проверка результата

// (с неявным преобразованием типа):

Console.WriteLine("Результат: "+c);

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

Как уже отмечалось, у класса Vector всего одно поле, описанное как

double[] coords — полем является переменная числового массива. Соз-

дание самого массива и заполнение его числовыми значениями вы-

полняется в конструкторе. У конструктора три аргумента. Командой

coords=new double[3] в теле конструктора сначала создается массив из

Перегрузка арифметических операторов и операторов приведения типа           157

трех элементов (координаты вектора), а затем аргументы конструктора

присваиваются элементам массива в качестве значений. Весь остальной

код класса — это перегрузка операторов. В частности, мы перегружаем би-

нарные операторы сложения (+) и вычитания (­) так, чтобы соответствую-

щие операции можно было выполнять с объектами класса Vector (которые

мы отождествляем с векторами).

Сразу обращаем внимание читателя на то, что оператор вычитания

(знак ­) может быть как бинарным, так и унарным. Если с бинарным

оператором все более-менее ясно, то унарный оператор — это знак

«минус» перед операндом, то есть команда вида ­a, где a — объект.

Обычно такая операция означает умножение на ­1. Именно в таком

смысле мы и понимаем такую унарную операцию.

А еще мы перегружаем оператор умножения (*) так, что в зависимости от

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

вектора на число или числа на вектор. Последние две операции коммута-

тивны — умножение вектора на число — это то же самое, что и умножение

числа на вектор. Кроме этого, можно будет делить вектор на число, а также

формально делить вектор на вектор. В последнем случае мы проявили ини-

циативу — в математике такая операция недопустима. Мы же определяем

ее так, что в результате возвращается значение угла между векторами. Пе-

регружаются операторы инкремента и декремента. Мы эти операции ин-

терпретируем, соответственно, как добавление к вектору единичного век-

тора того же направления и вычитание из вектора единичного вектора того

же направления. Вообще, одна и вторая операции довольно бесполезны, но

они более-менее соответствуют смыслу, который изначально вкладывался

в операторы инкремента и декремента.

Культурологическим шоком может стать перегрузка операторов приведе-

ния типа (или преобразования типа). Мы до этого вообще не подозревали, что такие операторы существуют. И где-то мы были правы. Однако этот

вопрос оставим на десерт, а сейчас вернемся к вещам более прозаическим.

Детальнее рассмотрим программный код перечисленных выше оператор-

ных методов.

ПРИМЕЧАНИЕ В программном коде использовано несколько встроенных математи-

ческих функций. Все они являются статическими методами класса

Math. Метод Sqrt() предназначен для вычисления квадратного корня

из числа, указанного аргументом функции. С помощью метода Sin() вычисляется синус от аргумента метода, а методом Acos() вычисляется

арккосинус от аргумента метода.

158

Глава 4. Перегрузка операторов

С перегрузкой оператора сложения все достаточно просто. Описывается

метод как Vector operator+(Vector a,Vector b), то есть мы имеем дело

с двумя операндами класса Vector и результатом — объектом того же клас-

са. В теле метода командой double[] x=new double[3] создается локаль-

ный массив из трех элементов, а заполняется массив в операторе цикла: индексная переменная i пробегает значение от 0 до 2 включительно, и для

каждого фиксированного значения индексной переменной выполняется

команда x[i]=a.coords[i]+b.coords[i]. В результате элементы масси-

ва x представляют собой суммы соответствующих элементов массивов-

полей объектов a и b (операнды в сумме). Создание нового объекта

с вычисленными параметрами (координатами) выполняется командой

Vector res=new Vector(x[0],x[1],x[2]). Объект res возвращается как ре-

зультат операторного метода.

Оператор умножения перегружается трижды: для вычисления скалярного

произведения векторов, для вычисления произведения вектора на число, и для вычисления произведения числа на вектор.

С программной точки зрения произведение объекта на число и числа

на объект — совершенно разные операции.

Операторный метод перегрузки оператора умножения для вычисления

скалярного произведения векторов описывается как double operator*(Ve ctor a,Vector b) — результатом является число типа double, а операнды —

объекты класса Vector. В теле операторного метода командой double res=0

объявляется и инициализируется с нулевым начальным значением ло-

кальная переменная res. Затем в операторе цикла эта переменная в резуль-

тате выполнения команды res+=a.coords[i]*b.coords[i] последовательно

увеличивается на попарное произведение координат объектов-операндов.

Индексная переменная i пробегает значения от 0 до 2. Вычисленное значе-

ние возвращается как результат.

Заголовок операторного метода перегрузки оператора умножения для вы-

числения произведения вектора на число имеет такой вид: Vector operator*

(Vector a,double b). Результатом операции является вектор (объект клас-

са Vector). Первый операнд также вектор, а второй операнд — число. В теле

метода командой сначала создаем массив x из трех элементов, а затем в опе-

раторе цикла заполняем его. Команда x[i]=a.coords[i]*b в теле оператора

цикла свидетельствует о том, что элементы массива x получаются умноже-

нием соответствующих элементов массива-поля первого операнда (объект

a) на второй числовой операнд (число b). Вычисленный в результате мас-

сив используется для создания нового объекта класса Vector. Этот объект

и возвращается в качестве результата операции.

Перегрузка арифметических операторов и операторов приведения типа           159

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

ла на вектор мы применяем маленькую военную хитрость — вызываем пе-

регруженный оператор умножения для вычисления произведения вектора

на число (инструкция a*b).

ПРИМЕЧАНИЕ Другим словами, операторный метод для вычисления произведения

числа на вектор возвращает результатом выражение, в котором век-

тор умножается на число. А на этот операторный метод перегружен

в явном виде. Такой подход не только экономит время и силы, но

имеет еще и далеко идущие последствия. Так, если в какой-то момент

мы решим изменить правила вычисления произведения вектора на

число (и числа на вектор), достаточно будет внести изменения только

в операторный метод для вычисления произведения вектора на число.

Произведение числа на вектор будет автоматически вычисляться по

тем же правилам.

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

Результат операторного метода Vector operator/(Vector a,double b) вы-

числяется по-военному просто как a*(1/b). Ситуация усложняется, если мы

начинаем делить вектор на вектор. Эта операция уже сама по себе является

стрессовой. Но мы не теряемся и в теле операторного метода double operator/

(Vector a,Vector b) объявляем две локальные переменные, cosinus и phi, —

авось на что сгодятся. Косинус угла между векторами вычисляем командой

cosinus=(a*b)/((double)a*(double)b). Это очень загадочная команда. Ин-

струкция a*b в числителе является командой вычисления скалярного про-

изведения векторов с помощью перегруженного оператора умножения (рас-

сматривался выше). Знаменатель представляет собой произведение двух

чисел, (double)a и (double)b. Это произведение вычисляется по правилу

вычисления самых обычных произведений самых обычных чисел. Причина

в том, что значением и выражения (double)a, и выражения (double)b явля-

ются числа типа double. Такой чудный эффект достигается благодаря пере-

грузке оператора явного приведения объекта класса Vector в значение типа

double. Этот метод описан с заголовком explicit operator double(Vector a

). Метод унарный и определяет способ приведения объектов класса Vector (аргумент операторного метода) к значению типа double (в соответствии с

ключевым словом double после инструкции operator). Здесь как бы тип ре-

зультата стал частью имени операторного метода. Ключевое слово explicit означает, что перегружается оператор явного приведения типа.

В качестве результата оператором явного преобразования типа возвраща-

ется значение Math.Sqrt(a*a). Это корень квадратный из скалярного про-

изведения вектора на самого себя — другими словами, это модуль вектора.

Поэтому результатом инструкции (double)a является модуль вектора a,

160

Глава 4. Перегрузка операторов

а результатом инструкции (double)b — модуль вектора b соответственно.

Результат выражения (a*b)/((double)a*(double)b) — косинус угла между

векторами. Сам угол вычисляем командой phi=Math.Acos(cosinus).

Приведение типа может быть явным и неявным. Поясним это. Си-

туация первая. Есть выражение (назовем это выражение наше_вы-

ражение) определенного типа (назовем этот тип наш_тип), а нам

очень хочется преобразовать значение этого выражения в значение

совершенно другого типа (назовем этот тип другой_тип). Команда

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

гой_тип)наше_выражение.  Перед  выражением  в  круглых  скобках

следует указать тот тип, к которому преобразуется значение выраже-

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

преобразования  значения  нашего_типа  в  значение  другого_типа, или нет. Например, вполне логичным может представиться преоб-

разование целого числа в число действительное, но крайне сложно

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

алгоритм, как приводить неприводимое.

Ситуация может быть еще более запутанной. Например, пришли мы

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

и предлагаем банкирам погасить кредит за счет наших нетривиаль-

ных познаний в области программирования. Принимая во внимание

высокий социальный статус программистов и безвыходность ситуа-

ции, банкиры принимают единственно верное решение — принять

заемщика на работу и погашать кредит вычетами из зарплаты (ко-

торая  значительно  выше  средней  зарплаты  по  промышленности).

Более того, во все местные отделения банка поступает распоряжение: впредь кредиты программистам на C# погашать путем приема оных

на работу. Все. Дримз кам тру!

Выше был пример неявного приведения типа, когда значение наше-

го_типа преобразуется в значение другого_типа без какой-либо явной

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

мы создаем сами, правила преобразования можно задать с помощью

операторных методов приведения типа. Оператор неявного преоб-

разования описывается с атрибутом implicit вместо explicit. С таким

оператором мы еще столкнемся.

Разность векторов вычисляется операторным методом Vector operator­

(Vector a,Vector b) как сумма первого вектора-операнда a со вторым

вектором-операндом b, умноженным на ­1 (команда a+(-1)*b). Но это еще

не все. Здесь оператор «минус» выступает как бинарный. Но он может

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

Перегрузка арифметических операторов и операторов приведения типа           161

точки зрения такая ситуация означает умножение на ­1. Именно так ее по-

нимаем и мы, когда перегружаем унарный оператор «минус» для вектора: результатом метода с заголовком Vector operator­(Vector a) является вы-

ражение (­1)*a, которое, кстати, вычисляется на основе перегруженного

оператора умножения числа на вектор.

Операторы инкремента и декремента для объектов класса Vector перегру-

жаются практически одинаково — различия минимальны. Результат для

оператора инкремента (заголовок метода Vector operator++(Vector a)) вычисляется как a=a+(a/(double)a). К вектору добавляется этот же вектор, деленный на свой модуль, и полученный результат присваивается в каче-

стве значения операнду и возвращается как результат. В операторе декре-

мента (заголовок операторного метода Vector operator­­(Vector a)) ре-

зультат вычисляется как a=a­(a/(double) a). От вектора отнимается этот

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

и является результатом метода.

При перегрузке унарных операторов инкремента и декремента в каче-

стве результата возвращается сам операнд. Другими словами, в резуль-

тате каждой из этих операций изменяется тот объект, который указан

аргументом операторного метода. Однако изменяется он специфиче-

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

a обозначен операнд. Командой a/(double)a создается новый объект, который соответствует вектору a, деленному на свой модуль. Такой век-

тор имеет единичную длину и ориентирован в пространстве так же, как

исходный вектор a. Далее, в результате выполнения инструкции a+(a/

(double)a) создается еще один новый объект, который соответствует

сумме векторов a и a/(double)a. До этих самых пор операнд a (аргу-

мент метода) не изменился. В результате выполнения команды a=a+

(a/(double)a) ссылка в переменной a с исходного объекта-аргумента

перебрасывается на объект a+(a/(double)a). Внешне иллюзия такая, что мы изменили аргумент операторного метода. На самом деле мы

создали новый объект, и на этот новый объект перебросили ссылку

в аргументе метода.

Можно было поступить иначе: изменить значения полей именно того

объекта,  который  передавался  аргументом  операторному  методу.

Стратегически это было бы более верно, но не так красиво.

Последний перегруженный оператор — это оператор неявного приведения

объекта класса Vector к текстовому значению (тип string). У этого оператор-

ного метода довольно хитрый заголовок implicit operator string(Vector a).

Этот заголовок по структуре очень похож на заголовок операторного ме-

тода явного приведения типа, за исключением того, что конечным типом

162

Глава 4. Перегрузка операторов

является string и вместо атрибута explicit использован атрибут implicit.

Как уже отмечалось, последний является признаком того, что речь идет

о неявном преобразовании типов.

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

месте, где по логике должно быть текстовое значение, окажется объ-

ект класса Vector. Механизм приведения типов представляет собой

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

приведения — или явную, или неявную.

В качестве результата при приведении объектов класса Vector к значению

типа string возвращается текстовая строка с координатами вектора (ко-

манда "<"+a.coords[0]+";"+a.coords[1]+";"+a.coords[2]+">").

С описанием класса Vector мы разобрались. Теперь обратимся к программ-

ному коду в главном методе программы. Основное его назначение — про-

верить, как вся эта кухня работает. Для этого мы объявляем три объектные

переменные, a, b и c, класса Vector. Также объявляются три числовые пере-

менные, phi, cosinus и expr, — результаты вычислений нужно куда-то запи-

сывать. Затем командами a=new Vector(3,0,4) и b=new Vector(0,6,8) созда-

ем два вектора и вычисляем угол межу ними командой phi=a/b.

Косинус угла между векторами можно посчитать командой cosinus=a*b/

((double)a*(double)b). Если все вычисления верны, то значением выражения

expr=Math.Sin(phi)*Math.Sin(phi)+cosinus*cosinus должна быть единица.

ПРИМЕЧАНИЕ Здесь  имеется  в  виду  тригонометрическое  тождество

2

2

sin (j )

(ϕ) + cos (j ) = 1

2

2

sin (j ) + cos (j )

(ϕ) = 1 .

Командой Console.WriteLine("Проверка: sin(phi)^2+cos(phi)^2="+expr) проверяем результат вычислений. Далее командами a++ и ­­b изменя-

ем объекты a и b и на основе новых их значений с помощью команды c=­

(a*5-b/2) вычисляем новый вектор (переменная c). После этого выпол-

няется команда Console.WriteLine("Результат: "+c), в которой объектная

переменная c использована вместе с текстовым литералом в аргументе ме-

тода WriteLine(). Это как раз тот случай, когда будет выполнено неявное

приведение типа (объекта класса Vector к значению типа string).

Существует  и  более  надежный  способ  конвертировать  объекты

в текстовые значения. Базируется он на переопределении метода

ToString(). Но об этом речь будет идти несколько позже.

Перегрузка операторов отношений           163

Результат выполнения программы показан на рис. 4.2.

Рис. 4.2.  Результат выполнения программы

с различными операторными методами

ПРИМЕЧАНИЕ Выше мы использовали оператор инкремента и декремента. Один из

них (оператор инкремента) вызывался в постфиксной форме, а другой

(оператор декремента) — в префиксной. В языке C# перегружаются

сразу  обе  формы  операторов  инкремента  и  декремента.  Другими

словами, если оператор инкремента (декремента) перегружен, то его

можно использовать как в префиксной, так и в постфиксной форме.

Причем обе формы работают одинаково за исключением того, как

обрабатывается выражение, содержащее оператор инкремента (де-

кремента). Правило здесь простое: если использована префиксная

форма оператора, то сначала изменяется операнд (то есть сначала

действует оператор), а уже после этого вычисляется значение вы-

ражения. Если использована постфиксная форма оператора, то сна-

чала вычисляется выражение, а уже после этого изменяется операнд

(действует оператор).

Перегрузка операторов отношений

Нас всех губит отсутствие дерзости

в перспективном видении проблем.

Мы не можем себе позволить фантази-

ровать. «От» и «до», и ни шага в сторону.

Вот в чем наша главная ошибка.

Из к/ф «Семнадцать мгновений весны»

Еще один пример, который мы рассмотрим в этой главе, также касается

перегрузки операторов, и в том числе операторов отношений. Особенность

операторов отношений состоит в том, что они перегружаются парами: на-

пример, если перегружен оператор > (больше), то придется перегрузить

и оператор < (меньше).

164

Глава 4. Перегрузка операторов

Другие пары: == (равно) и != (не равно), а также <= (меньше или

равно) и >= (больше или равно). Причем при перегрузке операторов

== и != необходимо также переопределить методы Object.Equals() и Object.GetHashCode(). Эти методы вызываются при сравнении объ-

ектов и должны быть синхронизированы с операторами равенства/

неравенства.

Однако перегрузкой лишь операторов отношений мы не ограничимся.

Мы снова будем перегружать арифметические операторы, но на этот раз

несколько иначе. Для перегрузки такого большого набора операторов

нам понадобится подходящий объект (в обычном смысле этого слова).

И такой объект у нас есть — это комплексное число. Мы опишем специ-

альный класс для реализации комплексных чисел и выполнения основ-

ных математических операций с этими числами. Некоторые операции

имеют общепризнанные математические аналоги. Некоторые мы домыс-

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

на предмет равно/не равно. Но операции сравнения больше/меньше для

комплексных чисел не определены, поскольку не имеют особого матема-

тического смысла. Мы устраним эту досадную оплошность. Для сравне-

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

ше модуль.

ПРИМЕЧАНИЕ Напомним, что комплексным числом  z  называется выражение вида

z = x + iy. Здесь  i  — мнимая единица (по определению  2

i = -1),

а  x   и  y  являются  действительными  числами  и  обозначаются  как

x = Re( z) (действительная часть комплексного числа) и  y = Im( z) (мнимая  часть  комплексного  числа).  Основные  арифметические

операции  с  комплексными  числами  выполняются  так  же,  как  и  с

действительными, лишь с поправкой на соотношение  2

i = -1. Резуль-

татом суммы двух комплексных чисел,  z 1 = x 1 + i 1

y  и  z 2 = x 2 + iy 2,

  называется  число  z 1 + z 2 = ( x 1 + x 2) + i( 1

y + y 2) (складывают-

ся отдельно действительные и мнимые части комплексных чисел).

Аналогично вычисляется разность  z 1 - z 2 = ( x 1 - x 2) + i( 1

y - y 2).

Произведением  двух  комплексных  чисел  называется  число

z z

1 1

× × z z

2 2==

( x( x

1 1 x

2 2

--1

y y 1

y y

2)2)

++ i( ix( x

2 y 21 1

y++ x x

1 y 1 y

2)2.)

  Частное  комплексных  чисел

z

x x + y y

x y - x y

вычисляется по формуле  1

1 2

1 2

2 1

1 2

=

+

i . Комплек-

2

2

2

2

z 2

x 2 + y 2

x 2 + y 2

сно спряженным к числу  z = x + iy  называется число  *

z = x - iy.

Модулем комплексного числа называется действительное неотрица-

тельное число

*

2

2

| z |= z × z = x + y .

Перегрузка операторов отношений           165

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

в области комплексных чисел, проанализируем программный код, пред-

ставленный в листинге 4.3.

Листинг 4.3.  Перегрузка операторов сравнения

using System;

// Класс для реализации комплексных чисел:

class Compl{

// Поле - действительная часть

// комплексного числа:

public double Re;

// Поле - мнимая часть комплексного числа:

public double Im;

// Конструктор класса с двумя аргументами:

public Compl(double x,double y){

Re=x; // Действительная часть

Im=y; // Мнимая часть

}

// Конструктор класса с одним аргументом:

public Compl(double x):this(x,0){}

// Оператор неявного приведения

// типа double к типу Compl:

public static implicit operator Compl(double a){

// Объект-результат создается

// на основе действительного числа:

return new Compl(a);

}

// Оператор явного приведения типа Compl к типу double:

public static explicit operator double(Compl a){

// Вычисляется модуль комплексного числа:

return Math.Sqrt(a.Re*a.Re+a.Im*a.Im);

}

// Оператор неявного приведения

// типа Compl к типу bool:

public static implicit operator bool(Compl a){

if(a.Im==0) return true; // Если число действительное

else return false; // Если есть мнимая часть

}

// Оператор неявного приведения

// типа Compl к типу string:

public static implicit operator string(Compl a){

// В условном операторе используем

// перегруженный оператор неявного

// приведения типа Compl к типу bool:

продолжение

166

Глава 4. Перегрузка операторов

Листинг 4.3 (продолжение)

if(a) return ""+a.Re; // Если действительное число

else{

// Если нулевая действительная часть:

if(a.Re==0) return a.Im+"i";

else return a.Re+((a.Im<0)?"":"+")+a.Im+"i"; // Все прочие

// случаи

}

}

// Оператор побитового отрицания

// перегружается для вычисления

// комплексно-сопряженного числа:

public static Compl operator~(Compl a){

// Комплексно-сопряженное число:

return new Compl(a.Re,-a.Im); // Меняет знак мнимая часть

}

// Оператор умножения комплексных чисел:

public static Compl operator*(Compl a,Compl b){

// Явно используем правило умножения

// комплексных чисел:

return new Compl(a.Re*b.Re-a.Im*b.Im,a.Re*b.Im+a.Im*b.Re);

}

// Оператор деления комплексных чисел:

public static Compl operator/(Compl a,Compl b){

// Результат определяем через

// перегруженные операторы умножения

// комплексных чисел и вычисления комплексно-

// сопряженного числа:

return a*(~b)*(1/(double)b/(double)b);

}

// Оператор сложения комплексных чисел:

public static Compl operator+(Compl a,Compl b){

// Явно используем правило сложения

// комплексных чисел:

return new Compl(a.Re+b.Re,a.Im+b.Im);

}

// Оператор вычитания комплексных чисел:

public static Compl operator-(Compl a,Compl b){

// Используем перегруженные операторы

// умножения и сложения комплексных чисел:

return a+(-1)*b;

}

// Перегрузка оператора "больше":

public static bool operator>(Compl a,Compl b){

// Сравниваются модули комплексных чисел:

return (double)a>(double)b;

Перегрузка операторов отношений           167

}

// Перегрузка оператора "меньше":

public static bool operator<(Compl a,Compl b){

// Сравниваются модули комплексных чисел:

return (double)a<(double)b;

}

// Перегрузка оператора "больше или равно":

public static bool operator>=(Compl a,Compl b){

// Сравниваются модули комплексных чисел:

return (double)a>=(double)b;

}

// Перегрузка оператора "меньше или равно":

public static bool operator<=(Compl a,Compl b){

// Сравниваются модули комплексных чисел:

return (double)a<=(double)b;

}

// Перегрузка оператора "равно":

public static bool operator==(Compl a, Compl b){

// Вызывается метод Equals():

return a.Equals(b);

}

// Перегрузка оператора "не равно":

public static bool operator!=(Compl a,Compl b){

// Вызывается метод Equals():

return !a.Equals(b);

}

// Переопределение метода Equals():

public override bool Equals(Object obj){

Compl b=obj as Compl;

// Отдельно сравниваются действительные

// и мнимые части чисел:

if((Re==b.Re)&(Im==b.Im)) return true;

else return false;

}

// Переопределение метода GetHashCode():

public override int GetHashCode(){

return Re.GetHashCode();

}

}

// Класс с главным методом программы:

class ComplDemo{

// Главный метод программы:

public static void Main(){

// Объекты для комплексных чисел:

Compl a=new Compl(4,-3);

продолжение

168

Глава 4. Перегрузка операторов

Листинг 4.3 (продолжение)

Compl b=new Compl(-1,2);

// Формируем текстовую строку:

string str="Арифметические операции:\n";

str+="a+b="+(a+b)+"\na-b="+(a-b)+"\na*b="+(a*b)+

"\na/b="+(a/b)+"\n";

str+="Операции сравнения:\n";

str+="a"+(ab->"+(a>b)+"\na<=b->"+(a<=b)+

"\na>=b->"+(a>=b);

str+="\na==b->"+(a==b)+"\na!=b->"+(a!=b);

// Проверка результатов вычислений:

Console.WriteLine(str);

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

Класс для реализации комплексных чисел называется Compl. У класса есть

два числовых (типа double) поля: поле Re для записи действительной ча-

сти комплексного числа и поле Im для записи мнимой части комплексного

числа. Также у класса есть два конструктора: конструктор с двумя аргу-

ментами и конструктор с одним аргументом. Если объект создается кон-

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

действительную и мнимую части комплексного числа. Если мы использу-

ем конструктор с одним аргументом, то этот аргумент определяет действи-

тельную часть комплексного числа, а мнимая равна нулю.

Инструкция  this(x,0)  в  определении  конструктора  класса

Compl(double x) с одним аргументом означает, что на самом деле

в  этом  случае  вызывается  конструктор  с  двумя  аргументами  —

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

перегрузке операторов. Конструктор имеет особое значение в силу

простого и очевидного обстоятельства: в математическом плане дей-

ствительные числа являются подмножеством множества комплексных

чисел.  Поэтому,  например,  действительное  число  —  это  частный

случай комплексного числа, у которого мнимая часть равна нулю.

Мы перегружаем несколько операторов приведения типов — в основном

неявного. Решающую роль в нашем деле имеет перегрузка оператора не-

явного приведения типа double к типу Compl. Заголовок этого оператора

имеет вид implicit operator Compl(double a). Тело операторного метода

состоит всего из одной команды return new Compl(a), которой в качестве

Перегрузка операторов отношений           169

результата метода возвращается объект класса Coml, созданный с помощью

конструктора с одним аргументом — действительным числом. Это имен-

но то число, которое преобразуется к типу Compl. Что это нам дает? Если

в какой-то команде или выражении в определенном месте вместо операнда

типа Compl встретится double-значение, это double-значение будет авто-

матически преобразовано в объект класса Compl. Эта ситуация полностью

соответствует математической сути проблемы. И этим мы неоднократно

воспользуемся при перегрузке арифметических операторов.

Также мы определяем обратное преобразование — объекта класса Compl в значение типа double. В этом случае в качестве результата возвраща-

ется модуль комплексного числа. Заголовок у оператора explicit oper ator double(Compl a), то есть в данном случае речь идет о явном приве-

дении типов. Оператор в качестве результата возвращает значение Math.

Sqrt(a.Re*a.Re+a.Im*a.Im). Таким образом, в результате явного приведе-

ния типа Compl к типу double в качестве результата возвращается модуль

комплексного числа.

Операторный метод неявного преобразования типа Compl к типу bool (за-

головок метода implicit operator bool(Compl a)) мы определяем так, что

для комплексных чисел с нулевой мнимой частью (когда число на самом

деле действительное) возвращается значение true. Если мнимая часть от-

лична от нуля, возвращается значение false.

Перегрузив оператор неявного приведения типа Compl к типу string, мы

обеспечиваем удобный механизм преобразования содержимого объекта

класса Compl в приемлемый текстовый формат. Под приемлемым форма-

том подразумевается общепринятый в математике способ написания ком-

плексных чисел. Метод с заголовком implicit operator string(Compl a) имеет немного запутанный код. В условном операторе проверяется, явля-

ется ли объект a представлением действительного числа. Это важно, по-

скольку в таком случае, очевидно, нет необходимости отображать мни-

мую часть. В условном операторе встречается инструкция if(a), которая

в обычных условиях не имела бы смысла. В скобках после ключевого слова

if должно быть выражение логического типа. Поскольку мы перегрузили

оператор неявного приведения к логическому типу, то тут все в порядке.

Если у числа мнимая часть нулевая, то это равносильно значению true в условии. В этом случае методом возвращается значение ""+a.Re (к пу-

стой текстовой сроке "" дописывается значение поля a.Re).

ПРИМЕЧАНИЕ Пустая текстовая строка нам понадобилась для автоматического пре-

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

преобразования объектов (и числовых переменных) в текст может

использоваться метод ToString().

170

Глава 4. Перегрузка операторов

Если число не является действительным, нам нужно проверить, отлична

ли от нуля действительная часть этого числа — нулевую действительную

часть отображать не принято. Опять используем условный оператор. Если

число является чисто мнимым, результатом операторного метода возвра-

щается текстовое выражение a.Im+"i" — к мнимой части мы приписыва-

ем букву i, обозначающую мнимую единицу. Однако может статься, что

и здесь нам не повезло — у числа есть как действительная, так и мнимая

части. Тогда актуальным становится вопрос, какого знака мнимая часть.

Для положительной мнимой части придется в явном виде вставить знак

"+". Такая вставка формируется с помощью тернарного оператора: резуль-

татом инструкции ((a.Im<0)?"":"+") является пустая текстовая строка "", если выполнено условие (a.Im<0), и текстовая строка "+" в противном слу-

чае. Вся текстовая строка, возвращаемая в качестве результата, определя-

ется выражением a.Re+((a.Im<0)?"":"+")+a.Im+"i".

Оператор побитового отрицания перегружаем для вычисления комплекс-

но-сопряженного числа. Заголовок этого операторного метода имеет вид

Compl operator~(Compl a). В качестве результата методом возвращается

новый объект new Compl(a.Re,-a.Im), который создается на основе объекта-

операнда заменой знака поля Im (мнимая часть числа).

Все, что мы рассмотрели выше, — предварительные приготовления. В бой

вступаем, переопределяя арифметические операторы.

Оператор умножения комплексных чисел описывается с заголовком Compl operator*(Compl a,Compl b), а значением является новый объект, который

создается инструкцией new Compl(a.Re*b.Re-a.Im*b.Im,a.Re*b.Im+a.Im*b.

Re). Здесь мы фактически в явном виде использовали правило вычисления

произведения двух комплексных чисел. По-хорошему стоило бы еще пере-

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

ствительные числа с комплексными, и наоборот. К счастью, здесь в этом

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

неявного приведения типа double к типу Compl. Поэтому если встретится

команда, в которой складывается значение типа double с объектом класса

Compl (не важно, в каком порядке), то, поскольку такая операция явно не

перегружена, double-аргумент будет автоматически приведен к типу Compl, и дальше команда обрабатывается в соответствии со всеми правилами

жанра.

Оператор деления комплексных чисел имеет заголовок Compl operator/

(Compl a,Compl b), и его результат вычисляется еще хитрее. Значение опе-

ратора вычисляется в виде выражения a*(~b)*(1/(double)b/(double)b).

Если подойти к вопросу формально, то результат вычисляется как про-

изведение первого операнда на комплексно-спряженный второй операнд

и делится на квадрат модуля второго операнда. Причем операция деле-

ния на квадрат модуля реализуется как умножение на единицу, деленную

Перегрузка операторов отношений           171

на квадрат модуля. При этом следует помнить, что модуль комплексного

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

комплексных чисел сведена к произведению комплексных чисел (два ком-

плексных и одно действительное, которое автоматически приводится к

формату комплексного числа). А оператор произведения уже перегружен.

Здесь  мы  воспользовались  рядом  тождеств.  Так,  если  a   и  b   —

*

*

a

a × b

a × b

1

комплексные числа, то

*

=

=

= a × b ×

. Все как

*

2

2

b

b × b

| b |

| b |

в жизни — все новое и незнакомое сводится к старому и хорошо

известному.

Просто обстоят дела с перегрузкой оператора сложения. Заголовок опе-

раторного метода имеет вид Compl operator+(Compl a,Compl b), а в каче-

стве результата возвращается объект new Compl(a.Re+b.Re,a.Im+b.Im). Как

и в случае с оператором произведения, здесь мы в явном виде используем

правило (или формулу) — только формулу сложения комплексных чисел.

Оператор вычитания комплексных чисел с заголовком Compl operator­

(Compl a,Compl b) очень прост — тело оператора состоит всего из одной

команды return a+(-1)*b. Здесь все просто и очевидно — разность двух

комплексных чисел вычисляется как сумма первого числа и второго, умно-

женного на ­1.

Что касается перегрузки операторов сравнения «больше», «меньше»,

«больше или равно» и «меньше или равно», то соответствующая операция

с комплексными числами придумана нами лично. Поэтому перегружа-

ем, как хотим. В частности, сводим все к сравнению модулей комплекс-

ных чисел. Например, оператор «больше» с заголовком bool operator> (Compl a,Compl b) в качестве результата возвращает значение выражения

(double)a>(double)b, которое представляет собой команду сравнения двух

действительных чисел и выполняется по классическим канонам.

Немного сложнее обстоят дела с перегрузкой операторов «равно» и «не

равно». Что касается самого кода перегружаемых операторных методов, то

он несложный. Например, оператор «равно» перегружается с заголовком

bool operator==(Compl a, Compl b). Как результат возвращается значение

выражения a.Equals(b) — из объекта a (первый операнд) вызывается ме-

тод Equals() с аргументом b (второй операнд). Оператор «не равно» пере-

гружается синхронно: у метода заголовок bool operator!=(Compl a,Compl b), а в качестве значения возвращается выражение !a.Equals(b). Таким об-

разом, если один из методов «равно» или «не равно» возвращает значение

true, то другой возвращает значение false. В основе перегрузки этих опе-

раторов — метод Equals(). Этот метод переопределяется в классе Compl.

172

Глава 4. Перегрузка операторов

Мы знаем, что если метод наследуется в производном классе из ба-

зового, то в производном классе унаследованный метод можно пере-

определить — заменить унаследованную версию метода на новую.

Переопределение не следует путать с перегрузкой. При перегрузке

создается  метод  с  таким  же  именем,  но  с  другой  сигнатурой.  При

переопределении метода сигнатуры старой и новой версий метода

совпадают. Фактически, переопределение метода означает, что он соз-

дается заново. Переопределение выполняется с атрибутом override.

Метод Equals() описан в классе Object, который находится в вершине

иерархии классов. Все классы, в том числе и те, что создаются нами, неявно являются наследниками класса Object. Основное назначение

метода  Equals()  —  сравнивать  переменные  и  объекты  на  предмет

«равно/не  равно».  Для  объектов  сравниваются  соответствующие

объектные переменные. По умолчанию сравнение дает значение ис-

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

Если нас такое поведение метода и такая интерпретация равенства

объектов не устраивает, мы этот метод переопределяем — что мы

и сделали.

Класс Object относится к пространству имен System. Альтернативным

обозначением класса System.Object является идентификатор object.

Мы будем пользоваться и тем и другим обозначениями. Это немного

напоминает ситуацию с текстовым классом.

При переопределении метода Equals() мы использовали следующий за-

головок: public override bool Equals(Object obj). Ключевое слово

override означает, что для данного класса (того, в котором переопределя-

ется метод, — в данном случае это класс Compl) ту версию метода, что была

унаследована из базового класса, необходимо заменить на новую. Именно

эта новая версия описывается при переопределении метода. Аргументом

метода является объект класса Object. Это обстоятельство нужно просто

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

дет на самом деле объект класса Compl. Поэтому в теле метода командой

Compl b=obj as Compl объявляется объектная переменная b и в качестве

значения ей присваивается ссылка на объект, переданный аргументом ме-

тоду Equals().

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

Есть одно важное обстоятельство. Состоит оно в том, что объектная

переменная базового класса может ссылаться на объект производного

Перегрузка операторов отношений           173

класса. Это одно из фундаментальных свойств наследования. След-

ствием является то, что вместо объекта базового класса аргументом

методу можно передать объект производного класса. Этим мы и поль-

зуемся (в части передачи аргументов) при переопределении метода

Equals(). Что касается команды Compl b=obj as Compl, то здесь ис-

пользован оператор as, который применяется для приведения типов.

Главное отличие от традиционного способа с указанием конечного

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

не удалась, в случае использования as-оператора возвращается пустая

ссылка и не генерируется ошибка.

После этого в условном операторе сравниваются действительные и мни-

мые части комплексных чисел (одно число спрятано в объекте, из которого

вызывается метод Equals(), а второе число спрятано в аргументе метода

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

чение false.

Переопределением метода Equals() дело не заканчивается. Желательно

переопределить еще и метод GetHashCode().

ПРИМЕЧАНИЕ В принципе, если не переопределить метод GetHashCode(), программа, скорее всего, будет откомпилирована — правда, с предупреждения-

ми. Мы этот метод переопределяем.

Связь между операторами «равно», «не равно» и методами Equals() и  GetHashCode()  достаточно  запутанная  и  местами  имеет  привкус

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

хэш-код. Каждая переменная или объект имеет свой хэш-код. Это це-

лое число, которое играет роль своеобразного идентификационного

кода для переменной или объекта. Узнать хэш-код можно с помощью

метода GetHashCode(), который описан в классе Object. Существует

такое  правило:  если  объекты  одинаковы  (то  есть  равны),  то  они

должны иметь одинаковый хэш-код. Но если объекты (переменные) имеют одинаковый хэш-код, это еще не означает их равенства. Хэш-

код — это первый рубеж в борьбе за равенство объектов. Если хэш-

коды объектов равны, то дальнейшая проверка на предмет равенства

выполняется с помощью метода Equals(). Мы переопределяем метод

GetHashCode() для того, чтобы метод возвращал одинаковые хэш-коды

для объектов, если они равны в нашем понимании.

Переопределение метода GetHashCode() выглядит совершенно баналь-

но и состоит всего из одной команды return Re.GetHashCode(), которая

означает, что в качестве значения методом возвращается хэш-код поля Re

174

Глава 4. Перегрузка операторов

объекта, из которого вызывается метод. В этом смысле мы делаем намек на

равенство объектов, у которых одинаковые действительные части.

ПРИМЕЧАНИЕ Хэш-код — это int-значение, то есть 32 бита. С помощью 32 битов

можно  записать  32

2  различных комбинаций нулей и единиц. Это

очень большое число. Но на фоне всех возможных значений дей-

ствительного числа  32

2  — сущие пустяки. Поэтому хэш-коды будут

повторяться. При переопределении метода GetHashCode() жела-

тельно добиться того, чтобы возвращаемый хэш-код был более-менее

уникальным (чтобы сузить множество потенциально эквивалентных

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

это волнует мало, и в качестве хэш-кода объекта комплексного числа

мы  используем  хэш-код  поля,  в  которое  записана  действительная

часть комплексного числа.

В главном методе программы командами Compl a=new Compl(4,-3) и Compl b=new Compl(-1,2) создаются объекты для комплексных чи-

сел a = 4 - 3 i и b = -1 + 2 i , после чего проверяются основные операции

с этими числами. Результат выполнения этой программы представлен на

рис. 4.3.

Рис. 4.3.  Результат выполнения программы с классом

для реализации комплексных чисел

Желающие могут проверить корректность вычислений или поупражнять-

ся в более изысканных калькуляциях на множестве комплексных чисел.

Свойства,

индексаторы

и прочая экзотика

Много лет размышлял я над жизнью земной,

Непонятного нет для меня под луной,

Мне известно, что мне ничего не известно,

Вот последняя тайна, открытая мной.

О. Хайям

В языке C# есть достаточно экзотические конструкции, с которыми нам

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

живают индексаторы и свойства. С ними мы и познакомимся в начале

главы. Также здесь мы рассмотрим ряд других важных тем, которые ка-

саются способов, с помощью которых данные упаковываются в объектах.

Достаточно важный вопрос, которому мы также уделим внимание в этой

главе, — это делегаты. Вообще делегаты предназначены для работы с мето-

дами, но важны в первую очередь потому, что через них реализуется систе-

ма обработки событий — неотъемлемая часть приложения с графическим

интерфейсом.

Мы уже немного знакомы с этой темой. Чтобы продвинуться дальше, нам

необходимо познакомиться с тем, как в C# обрабатываются события. Без

этого создать серьезное приложение с графическим приложением крайне

проблематично. Но делегаты и события — на закуску. А начнем мы с во-

просов более прозаичных.

176

Глава 5. Свойства, индексаторы и прочая экзотика

Свойства

Это мелочи. Но нет ничего важнее мелочей!

Из к/ф «Приключения Шерлока Холмса

и доктора Ватсона. Знакомство»

Свойство в C# — это нечто среднее между методом и полем. Свойство яв-

ляется симбиозом поля и методов для обработки этого поля. Другими сло-

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

специальные методы, которые автоматически вызываются при обращении

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

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

автоматически вызывается при считывании значения свойства. Оба этих

очень специальных метода называются аксессорами. Аксессоры специфич-

ны не только по сути, но и по форме — описываются они очень необычным

образом. Чтобы понять, что же такое, в конце концов, свойство и как свой-

ство связано с аксессорами, рассмотрим общий шаблон описания свой-

ства:

тип_свойства имя_свойства{

// Аксессор для считывания значения свойства:

get{

// Код get-аксессора

}

// Аксессор для присваивания значения свойству:

set{

// Код set-аксессора

}

}

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

ст ва и имя свойства — все так же, как и для обычного поля.

Обычно с идентификатором типа свойства указывается и ключевое

слово public — если мы хотим, чтобы свойство было доступно вне

пределов  класса.  Также  обратите  внимание  на  то,  что  аксессоры

описываются без круглых скобок!

Далее нас встречает сюрприз в виде пары фигурных скобок, в которых

описаны аксессоры. Как отмечалось выше, аксессоров два. Аксессор, ко-

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

словом get. После этого ключевого слова в фигурных скобках указывается

Свойства           177

программный код get-аксессора. Этот программный код выполняется

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

граммный код get-аксессора — это те команды, которые выполняются каж-

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

Поскольку в результате выполнения таких команд должно возвращаться

значение (значение свойства), get-аксессор описывается как метод, воз-

вращающий значение. Тип возвращаемого значения совпадает с типом

свойства.

При присваивании свойству значения вызывается set-аксессор. Код это-

го аксессора описывается в фигурных скобках после ключевого слова set.

Каждый раз, когда свойству присваивается значение, выполняются коман-

ды set-аксессора.

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

Если свойство описано только с get-аксессором, то такое свойство

можно прочитать, но ему нельзя присвоить значение. Если свойство

описано только с set-аксессором, то ему можно присвоить значение, но нельзя его прочитать.

При описании set-аксессора обычно используется ключевое слово value, которое обозначает присваиваемое свойству значение. Но и это еще не все.

У свойств есть еще одна маленькая, но вместе с тем довольно-таки большая

тайна. Чтобы ее раскрыть, обратимся к программному коду, представлен-

ному в листинге 5.1.

Листинг 5.1.  Знакомство со свойствами

using System;

class SmallNumber{

// Закрытое поле для "запоминания"

// значения свойства:

private int number;

// Свойство (целочисленное):

public int num{

// Аксессор для считывания значения свойства:

get{

// В качестве значения свойства

// num возвращается значение

// закрытого поля number:

return number;

}

продолжение

178

Глава 5. Свойства, индексаторы и прочая экзотика

Листинг 5.1 (продолжение)

// Аксессор для присваивания значения

// свойству:

set{

// Присваивается значение закрытому

// полю number - остаток от деления

// присваиваемого значения

// (инструкция value) на 10:

number=value%10;

}

}

// Конструктор класса с одним аргументом:

public SmallNumber(int n){

// Присваивается значение свойству:

num=n;

}

}

// Класс с главным методом программы:

class SmallNumberDemo{

// Главный метод программы:

public static void Main(){

// Создание объекта класса SmallNumber:

SmallNumber obj=new SmallNumber(107);

Console.WriteLine("Значение свойства: "+obj.num); obj.num=213;

Console.WriteLine("Значение свойства: "+obj.num); Console.ReadLine();

}

}

Программный код достаточно простой, но вместе с тем и довольно пока-

зательный. У класса SmallNumber описано закрытое целочисленное поле

number. Это поле нам крайне необходимо, и оно напрямую связано со свой-

ством num. Свойство описано с двумя аксессорами. Программный код get-

аксессора состоит всего из одной команды return number. Это означает, что

каждый раз при обращении к свойству num на самом деле считывается зна-

чение поля number. Лаконичен и программный код set-аксессора. При при-

сваивании значения свойству num выполняется команда number=value%10.

Инструкция value здесь обозначает то значение, которое присваивается

свойству. Точнее, это значение выражения, которое стоит справа от опера-

тора присваивания в команде присваивания значения свойству. Команда

означает, что полю number в качестве значения присваивается остаток от

деления на десять значения выражения, указанного справа от оператора

присваивания.

Свойства           179

Фактически,  set-аксессор  свойства  обрабатывает  команду  вида

свойство=value.

Помимо закрытого поля и открытого свойства, у класса SmallNumber есть

конструктор с одним аргументом. В теле конструктора свойству num при-

сваивается значение аргумента конструктора. Вот такой простой класс.

В главном методе программы командой SmallNumber obj=new SmallNumber (107) создается объект obj класса SmallNamber. Аргументом конструктору

передано значение 107, и это означает, что такое значение присваивается

свойству num, а в поле number будет записано значение 7 (остаток от деления

107 на 10). При обращении к свойству num объекта obj в команде Console.

WriteLine("Значение свойства: "+obj.num) возвращается именно значе-

ние 7. Если мы присваиваем значение свойству num командой obj.num=213, свойство получает значение 3. Результат выполнения программы проил-

люстрирован рис. 5.1.

Рис. 5.1.  Знакомство со свойствами — результат выполнения программы

Мораль очень простая — хотя свойство внешне ведет себя как поле, само по

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

описали в классе свойство, это еще не означает, что в памяти появилось ме-

сто для запоминания значения этого свойства. Чтобы было, куда записать

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

представляет собой некую оболочку, в которую упаковано обычное (как

правило, закрытое) поле (или нечто другое).

Хотя за свойством чаще всего прячется обычное поле (или несколь-

ко полей), такой подход не является необходимым. При описании

свойства достаточно предусмотреть корректность программного кода

аксессоров — всех, сколько их там есть. Если в классе имеется get-

аксессор, этот аксессор должен возвращать результат. А как он это

будет делать (на основе значения поля или как-то еще) — не прин-

ципиально. Что касается set-аксессора, то здесь у нас еще больше

свободы, ведь аксессор даже результата не возвращает.

180

Глава 5. Свойства, индексаторы и прочая экзотика

На первый взгляд может показаться, что в свете вышесказанного смысл

в использовании свойств полностью нивелируется. Тем не менее это не так.

Существует как минимум несколько ситуаций, когда свойства могут проя-

вить себя во всей красе. Один из таких случаев — когда нам нужно создать

поле, значение которого зависит от значений нескольких других полей. Ко-

нечно, вместо поля можно использовать метод, но такой подход не всегда

приемлем. Поэтому можно реализовать свойство. Пример такой ситуации

проиллюстрирован в программном коде в листинге 5.2.

Листинг 5.2.  Свойство без set-аксессора

using System;

// Класс со свойством:

class Box{

// Открытые поля:

public double width;

public double height;

public double depth;

// Свойство с одним аксессором:

public double volume{

// У свойства только get-аксессор:

get{

// Значение свойства определяется

// как произведение полей:

return width*height*depth;

}

}

// Конструктор класса с тремя аргументами:

public Box(double w,double h,double d){

// Полям присваиваются значения:

width=w;

height=h;

depth=d;

}

}

// Класс с главным методом программы:

class BoxDemo{

// Главный метод программы:

public static void Main(){

// Создание объекта:

Box obj=new Box(10,20,30);

// Обращение к свойству:

Console.WriteLine("Объем равен: "+obj.volume);

Console.ReadLine();

}

}

Свойства           181

В программе описан класс Box с тремя открытыми полями типа double.

Эти поля мы отождествляем с ребрами параллелепипеда. Объем такого

параллелепипеда определяется как произведение длин ребер (произве-

дение значений полей). В классе для считывания объема определяется

свойство volume. Особенность этого свойства состоит в том, что у него нет

set-аксессора. Поэтому присвоить значение свойству нельзя. Зато мож-

но прочитать значение свойства. В качестве значения свойства volume get-аксессором возвращается результат произведения трех полей (width, height и depth). В качестве иллюстрации использования свойства volume в главном методе программы создается объект класса Box и после этого вы-

полняется обращение к свойству volume этого объекта. Результат представ-

лен на рис. 5.2.

Рис. 5.2.  Свойство с одним аксессором —

результат выполнения программы

Как отмечалось выше, можно описать свойство с одним только set-аксес-

сором. Такому свойству можно присвоить значение, но нельзя прочитать

значение. Данного типа свойство — это своеобразный компромисс между

открытым и закрытым полем, поскольку свойство ведет себя при записи

значения как открытое поле, а при считывании значения — как закрытое

поле. Это же замечание, с очевидной рокировкой присваивания/считыва-

ния, относится и к свойству с единственным get-аксессором. Рассмотрим

небольшой пример, представленный в листинге 5.3.

Листинг 5.3.  Свойство без get-аксессора

using System;

// Класс со свойством без get-аксессора:

class MyNums{

// Закрытое поле - числовой массив:

private int[] nums;

// Метод для отображения содержимого массива:

public void show(){

// Перебираются элементы массива:

for(int i=0;i

// В консоль выводится значение

// элемента массива:

продолжение

182

Глава 5. Свойства, индексаторы и прочая экзотика

Листинг 5.3 (продолжение)

Console.Write(nums[i]+" ");

}

// Переход к новой строке:

Console.WriteLine();

}

// Свойство для считывания нового

// элемента массива:

public int next{

// Аксессор присваивания значения свойству:

set{

// Проверяем, существует ли массив:

if(nums==null){

// Массива нет - создаем массив из одного элемента:

nums=new int[1];

// Элементу массива присваивается значение:

nums[0]=value;

}

else{ // Массив уже существует

// Создаем локальный массив.

// Размер - на один элемент больше,

// чем у массива nums:

int[] t=new int[nums.Length+1];

// В локальный массив копируем значения

// элементов массива nums:

for(int i=0;i

t[i]=nums[i];

}

// Последний элемент локального

// массива - значение свойства:

t[nums.Length]=value;

// Переменная nums теперь ссылается на

// вновь созданный массив:

nums=t;

}

}

}

}

// Класс с главным методом программы:

class MyNumsDemo{

// Главный метод программы:

public static void Main(){

// Создание объекта:

MyNums obj=new MyNums();

// Заполнение поля-массива путем

Свойства           183

// присваивания значения свойству next:

for(int i=1;i<=20;i++){

obj.next=2*i-1;

}

// Отображение содержимого массива:

obj.show();

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

У класса MyNums есть закрытое поле — целочисленный массив nums. Есть

у класса открытый метод show(), которым элементы массива nums выводят-

ся в консоль (в одну строку через пробел). Также у класса имеется свой-

ство next, назначение которого состоит в том, чтобы дописывать элементы

в конец массива. У свойства есть set-аксессор, и нет get-аксессора. Пикант-

ность ситуации в том, что у класса MyNums нет конструктора, а поле nums по

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

иметь в виду, что массива может и не быть. В этом случае переменная мас-

сива nums имеет в качестве значения так называемую пустую ссылку, кото-

рая обозначается ключевым словом null. Поэтому алгоритм присваивания

значения свойству next такой.

1. Проверяем значение переменной nums, чтобы определить, существует ли

соответствующий массив.

2. Если массив не существует (значение переменной nums равно null), создаем массив из одного элемента и записываем в этот массив присваи-

ваемое свойству next значение.

3. Если массив nums существует, создаем новый локальный массив с раз-

мером, на один элемент больше чем массив nums. Начальные значения

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

значений из массива nums. Остается незаполненным один, последний

элемент локального массива. Этому массиву в качестве значения при-

сваиваем то значение, которое присваивается свойству next. После этого

ссылку на новый массив записываем в переменную-поле nums.

Именно такой алгоритм реализуется в программном коде set-аксессора

свойства next.

В главном методе программы создается объект класса MyNums и затем в опе-

раторе цикла, через присваивание значения свойству next этого объек-

та, формируется поле-массив из нечетных натуральных чисел. Методом

show() объекта результат отображается в консольном окне. Результат вы-

полнения программы показан на рис. 5.3.

184

Глава 5. Свойства, индексаторы и прочая экзотика

Рис. 5.3.  Свойство без get-аксессора — результат выполнения программы

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

свойств. Но эффективность языка программирования как раз во многом

и определяется его гибкостью, когда одна и та же задача может решаться

по-разному.

Индексаторы

Ну, хватит! Что вы словно мальчик пускаете

туман? Или вас зовут Монте-Кристо?

Из к/ф «Семнадцать мгновений весны»

Через индексаторы в C# реализуется механизм индексации объектов. Если

в классе описан индексатор, то объекты этого класса можно будет индек-

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

целочисленным, но может таковым и не быть. В некотором отношении

индексаторы напоминают свойства, с той разницей, что если за свойством

обычно прячется поле, то за индексатором, как правило, скрывается мас-

сив. Хотя это и не обязательно.

Как и у свойства, у индексатора есть аксессоры: set-аксессор предназначен

для присваивания индексатору значения, и get-аксессор предназначен для

считывания значения индексатора.

Когда мы делаем заявление о присваивании значения индексатору, обычно это подразумевает присваивание значения элементу масси-

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

значения индексатора: это может быть как элемент массива, так и про-

сто вычисляемое  значение.  Во многом  положение  дел напоминает

случай со свойствами. Но здесь ситуация сложнее, поскольку, помимо

индексируемого объекта, есть еще и значение индекса, который, в из-

вестном смысле играет роль аргумента метода-аксессора.

Индексаторы           185

Общий шаблон объявления индексатора такой:

тип_индексатора this[тип_индекса индекс]{

// Аксессор для считывания значения индексатора:

get{

// Программный код get-аксессора

}

// Аксессор для присваивания значения индексатору:

set{

// Программный код set-аксессора

}

}

При описании индексатора используется ключевое слово this, которое

является, напомним, ссылкой на объект — в данном случае индексируе-

мый. Для индексатора указывается тип возвращаемого/присваиваемого

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

сываются два аксессора. Если индексированный объект используется для

присваивания такой конструкции значения, выполняется программный

код set-аксессора. Индикатором присваиваемого значения служит ключе-

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

аксессора. Поэтому get-аксессор должен возвращать значение (тип резуль-

тата совпадает с типом индексатора).

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

ти классических вариантов представлен в листинге 5.4.

Листинг 5.4.  Знакомство с индексаторами

using System;

// Класс с индексатором:

class NumList{

// Закрытое поле-целочисленный массив:

private int[] nlist;

// Конструктор класса:

public NumList(int n){

// Создание массива.

// Размер массива определяется

// аргументом конструктора:

nlist=new int[n];

}

// Индексатор (целочисленный):

public int this[int index]{

продолжение

186

Глава 5. Свойства, индексаторы и прочая экзотика

Листинг 5.4 (продолжение)

// Аксессор для считывания

// значения индексатора:

get{

// Если индекс -1, значением является размер массива:

if(index==­1) return nlist.Length;

// В остальных случаях возвращается

// значение элемента массива:

else return nlist[index];

}

// Аксессор для присваивания

// значения индексатору - значение

// записывается в элемент массива

// с указанным индексом:

set{

nlist[index]=value;

}

}

}

// Класс с главным методом программы:

class NumListDemo{

// Главный метод программы:

public static void Main(){

// Создание объекта с полем-массивом:

NumList obj=new NumList(15);

// Заполняем первые два элемента массива.

// Для этого используем индексатор:

obj[0]=1;

obj[1]=1;

// Отображение первых двух элементов массива.

// Снова обращаемся к помощи индексатора:

Console.Write(obj[0]+" "+obj[1]);

// Заполнение элементов массива путем

// использования индексатора.

// Размер массива вычисляется инструкцией obj[-1]:

for(int i=2;i

// Заполняем массив числами Фибоначчи:

obj[i]=obj[i-1]+obj[i-2]; // Вычисляем новый элемент

Console.Write(" "+obj[i]); // Отображаем результат

}

Console.WriteLine(); // Переход к новой строке

Console.ReadLine(); // Ожидание нажатия клавиши Enter

}

}

Индексаторы           187

Результат выполнения этой программы представлен на рис. 5.4.

Рис. 5.4.  Знакомство с индексаторами — результат выполнения программы

В классе NumList спрятан целочисленный массив nlist. Это закрытое поле, так что доступа вне пределов класса к этому полю нет. У конструктора

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

В конструкторе же этот массив и создается. Еще в классе есть индексатор, описание которого начинается инструкцией public int this[int index]. Эта

дивная конструкция означает, что индексатор открытый (атрибут public), целочисленный (то есть значение индексатора — целое число — об этом

свидетельствует атрибут int перед ключевым словом thin). В квадратных

скобках инструкция int index означает, что индекс в программном коде

аксессоров будет соответствовать ключевому слову index и этот индекс яв-

ляется целым числом. Далее описаны программные коды аксессоров.

Программный код set-аксессора состоит всего из одной команды

nlist[index]=value. Эта команда означает, что в результате выполнения

команды вида объект[индекс]=значение, элементу массива nlist объек­

та с данным индексом будет присвоено данное значение. Несколько более

сложный код у get-аксессора. Вообще-то, и в этом случае можно было

обойтись малыми жертвами и ограничить весь код инструкцией вида

return nlist[index], которой в качестве значения индексатора возвраща-

ется элемент массива nlist с соответствующим индексом. Но нас такая

банальная ситуация не устраивает, и мы хотим, чтобы с помощью индек-

сатора можно было бы узнать не только значение того или иного элемент

массива nlist, но и размер этого массива. И мы придумали военную хи-

трость: если указываем индекс ­1, возвращается размер массива nlist.

Именно поэтому в get-аксессоре присутствует условный оператор. Если

индекс равен ­1, индексатором возвращается значение nlist.Length. Если

индекс не равен ­1, индексатором возвращается значение элемента масси-

ва с соответствующим индексом.

В главном методе программы мы создаем объект obj класса NumList. Эле-

менты массива этого объекта заполняются числами Фибоначчи. При этом

обращение к элементу массива nlist объекта obj с индексом i выполняется

в формате obj[i], причем как при считывании значения, так и при при-

сваивании значения.

188

Глава 5. Свойства, индексаторы и прочая экзотика

Стоит заметить, что массив nlist является закрытым полем, поэтому

права обращаться напрямую к его элементам у нас нет. В том числе

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

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

конечно, можно было бы определить свойство, предназначенное для

этих целей. Но что сделано, то сделано.

У индексаторов есть некоторые весьма интересные характеристики. Не-

которые из них мы уже упоминали. Все же перечислим те из характерных

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

интерес.

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

дать иллюзию существования такого массива.

 Индексатор может иметь как два аксессора, так и всего один аксессор: только set-аксессор (такому индексатору можно лишь присвоить зна-

чение, но нельзя прочитать значение такого индексатора) или только

get-аксессор (с помощью такого индексатора можно лишь прочитать

значение, но нельзя значение присвоить).

 У индексаторов может быть несколько индексов — как в многомерном

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

 Индекс у индексатора не обязательно должен быть целочисленным.

 Индексатор можно перегружать. Другим словами, у класса может быть

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

индексов.

Еще один пример использования индексаторов можно найти в программе, представленной в листинге 5.5.

Листинг 5.5.  Перегрузка индексаторов

using System;

// Класс с индексатором для реализации векторов:

class Vect{

// Закрытое поле - массив для записи

// координат вектора

private double[] x;

// Конструктор класса с тремя аргументами:

public Vect(double x1,double x2,double x3){

x=new double[3]{x1,x2,x3};

}

Индексаторы           189

// Метод для отображения координат вектора:

public void show(){

// Обратите внимание на аргументы метода WriteLine():

Console.WriteLine("Вектор: <{0};{1};{2}>",x[0],x[1],x[2]);

}

// Индексатор с целочисленным индексом:

public double this[int i]{

get{ // Возвращается значение координаты вектора:

return x[i%3]; // Обратите внимание на индекс

}

set{ // Координате присваивается значение:

x[i%3]=value; // Обратите внимание на индекс

}

}

// Индексатор с индексом - объектом:

public Vect this[Vect b]{

get{ // Вычисление векторного произведения

Vect c=new Vect(0,0,0); // Создание объекта

for(int i=0;i<3;i++){ // Вычисление координат вектора

// Используем индексатор:

c[i]=this[i+1]*b[i+2]-this[i+2]*b[i+1];

}

// Возвращаемое индексатором значение:

return c;

}

}

// Индексатор с двумя индексами - объектами:

public Vect this[Vect b,Vect c]{

get{

// Вычисление двойного векторного

// произведения:

return this[b[c]]; // Возвращаемое индексатором

// значение

}

}

}

// Класс с главным методом программы:

class VectDemo{

//Главный метод программы:

public static void Main(){

// Создание объектов:

Vect a=new Vect(1,3,2);

Vect b=new Vect(2,0,-1);

// Векторное произведение:

a[b].show(); // Использовали анонимный объект

продолжение

190

Глава 5. Свойства, индексаторы и прочая экзотика

Листинг 5.5 (продолжение)

// Еще один объект:

Vect c=new Vect(1,2,0);

// Двойное векторное произведение:

a[b,c].show(); // Использовали анонимный объект

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

Здесь мы снова обратились к теме реализации векторов (в трехмерном де-

картовом пространстве) с помощью специального класса. Правда, на сей

раз мы подходим к задаче очень избирательно: предусматриваем возмож-

ность запоминания координат вектора в специальном массиве (называет-

ся x), а также реализуем с помощью индексаторов процедуру вычисления

векторного и двойного векторного произведения.

 

ПРИМЕЧАНИЕ Векторным  произведением  c = a ´ b   векторов  a = (

1

a , a 2, a 3)

и  b = ( 1

b , 2

b , 3

b )  называется  вектор  c = ( 1

c , 2

c , c 3) с координатами

1

c = a 2 3

b - a 3 2

b ,  2

c = a 3 1

b - a 1 3

b  и  c 3 = 1

a 2

b - a 2 1

b . Три последние

формулы можно записать в общем виде как  k

c = ak 1 kb 2 - ak 2 kb

+

+

+

1

+

(индекс  k = 1,2,3) при условии циклической перестановки индексов: индекс 4 следует интерпретировать как индекс 1, а индекс 5 должен

интерпретироваться как индекс 2. Именно этим обстоятельством мы

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

аргументом — там вместо индекса берется остаток от деления на 3

(напомним, что индексация элементов массива начинается с нуля).

Такой подход не только обеспечил попадание любого целочислен-

ного  индекса  индексатора  в  допустимый  диапазон,  но  и  серьезно

упростил задачу по вычислению векторного произведения (которое

реализуется через индексатор с индексом-объектом).

Двойное векторное произведение мы вычисляем как выражение вида

a ´ ( b ´ c).  Двойное векторное произведение реализуется через

индексатор с двумя индексами-объектами.

У класса ест конструктор с тремя аргументами, которые задают коорди-

наты вектора, а также метод show(), предназначенный для отображения

в консольном окне сообщения с информацией о значениях координат век-

тора (значения элементов массива-поля соответствующего объекта).

Мы определяем три разных индексатора. Один индексатор подразумевает

наличие одного целочисленного индекса. Это классический индексатор, который мы используем для доступа к элементам поля-массива x. Вме-

сте с тем при обращении к элементу массива индекс этого элемента мы

Индексаторы           191

определяем как остаток от деления на 3 индекса индексатора. Такой под-

ход обеспечивает циклическую перестановку индекса элементов массива, если формально индекс индексатора превышает максимально допустимое

значение 2.

Обратите внимание на способ передачи аргументов методу WriteLine() в методе show() класса Vect. Первым аргументом передана текстовая

строка, которая, помимо непосредственно текста, содержит инструк-

ции {0}, {1} и {2} (то есть цифры в фигурных скобках). Этими ин-

струкциями помечены места вставки (при выводе в консоль) в тек-

стовую строку значений аргументов, переданных методу WriteLine() после  первого  текстового  аргумента.  Цифра  в  фигурных  скобках

означает порядковый номер такого аргумента. Нумерация начина-

ется с нуля, поэтому вместо инструкции {0} вставляется первый по

порядку аргумент после текстового аргумента (в данном случае это

x[0]), вместо инструкции {1} вставляется второй аргумент (значение

x[1]), и, наконец, инструкция {2} заменяется при выводе на значение

элемента x[2].

Индексатор с индексом-объектом определяется так, чтобы в результате

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

торного произведения векторы реализуются через объекты — объект, ко-

торый индексируется и объект, который указан в качестве индекса. Описа-

ние такого индексатора начинается инструкцией public Vect this[Vect b]

и этот индексатор имеет только get-аксессор. При вызове аксессора

создается объект c класса Vect, а затем с помощью оператора цикла вы-

числяются элементы поля-массива. Для фиксированного значения ин-

дексной переменной массива i вычисления выполняются командой

c[i]=this[i+1]*b[i+2]-this[i+2]*b[i+1]. В этой команде мы уже исполь-

зуем индексатор с целочисленным индексом для обращения к элементам

массивов, «спрятанных» в соответствующих объектах. При этом инструк-

ция this[i] означает обращение к объекту, из которого вызывается индек-

сатор (объект перед квадратными скобками). Объект c командой return c возвращается в качестве результата get-аксессора.

Также у класса имеется индексатор с двумя индексами-объектами. Этот

индексатор предназначен для вычисления двойного векторного произ-

ведения. Описание индексатора начинается инструкцией public Vect this[Vect b,Vect c], и, как и в предыдущем случае, у индексатора толь-

ко один аксессор. Значение индексатора, возвращаемое при обращении

к нему, вычисляется инструкцией this[b[c]] — сначала вычисляется объ-

ект b[c], а затем этот объект передается индексом объекту, который ин-

дексируется.

192

Глава 5. Свойства, индексаторы и прочая экзотика

В главном методе программы создаем три объекта, a, b и c, класса Vect. За-

тем нам встречаются две довольно любопытные команды: a[b].show() (вы-

числение векторного произведения и отображение результата) и a[b,c].

show() (вычисление двойного векторного произведения и отображение

результата). Здесь мы используем так называемые анонимные объекты.

Все дело в том, что, когда объект создается, используется оператор new. Ре-

зультатом вызова оператора является ссылка на вновь созданный объект.

Обычно эту ссылку записывают в объектную переменную. Но последнее

не является обязательным. Другими словами, объект создается вне зависи-

мости от того, записали мы ссылку на него в переменную или нет. Другое

дело, что, если ссылка на объект никуда не записана, он очень быстро по-

теряется — у нас не будет возможности обратиться к этому объекту. Все

обстоит иначе, если объект нам нужен только один раз, то есть он исполь-

зуется всего в одной команде. Тогда этот объект можно использовать без

присваивания ссылки на объект объектной переменной. Такие объекты

(объекты без имени) называются анонимными. Например, результатом вы-

полнения команды a[b] является объект, вычисляемый на основе объектов

a и b по правилу расчета векторного произведения. Ссылку на этот объект

мы можем записать в объектную переменную, а можем и не записывать.

Так мы и поступили: вместо того, чтобы присваивать ссылку на объект

a[b] в качестве значения объектной переменной, мы вызвали метод show() сразу из объекта a[b]. В результате получилась команда a[b].show(). Ана-

логично мы поступили при вычислении двойного векторного произведе-

ния — воспользовались командой a[b,c].show(), в которой метод show() вызывается из анонимного объекта a[b,c]. Результат выполнения нашей

программы представлен на рис. 5.5.

Рис. 5.5.  Перегрузка индексатора — результат выполнения программы

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

показаться, что индексатор представляет собой очень уж экзотическую

конструкцию, тем не менее в сочетании с механизмом перегрузки операто-

ров он становится грозным оружием в борьбе за написание непонятных, но

исправно работающих кодов. Что касается экзотики, то наше представле-

ние о ней сильно изменится после того, как мы познакомимся с делегатами

и событиями.

Делегаты           193

Делегаты

Его связи там важнее его самого здесь.

Из к/ф «Семнадцать мгновений весны»

Проведем маленькую ревизию некоторых наших познаний в области ООП.

Итак, что мы знаем?

 Объектная переменная может ссылаться на объект.

 Переменная массива может ссылаться на массив.

Делегат является продолжением этой логической цепочки. С помощью

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

методы. Использование делегатов подразумевает успешную реализацию

двух этапов. Это

 объявление делегата;

 реализация делегата, или создание экземпляра делегата.

Чтобы все это легче было понять, можно провести некоторую аналогию.

Объявление делегата сродни описанию класса, а реализация делегата (соз-

дание экземпляра делегата) соответствует созданию объекта класса. Итак, приступим к делу.

Экземпляр делегата предназначен для ссылки на метод. Понятно, что для

разных типов методов нужны разные делегаты. А что в методе важно? В ме-

тоде важен тип результата, а также количество и тип аргументов. Именно

эти два момента должны быть отражены при описании делегата. Делегаты

объявляются в соответствии со следующим шаблоном:

delegate тип_результата имя(список_аргументов);

Ключевое слово delegate является неотъемлемой частью инструкции объ-

явления делегата. После этого указывается ключевое слово-идентификатор

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

Затем указывается имя делегата и в круглых скобках список аргументов

метода.

ПРИМЕЧАНИЕ Тип результата и список аргументов относятся к методу. Делегат объ-

является для методов, которые возвращают результат определенного

типа и которым передается определенный список аргументов. А вот

имя делегата — аналог имени класса. Имя делегата используется при

создании экземпляра делегата.

194

Глава 5. Свойства, индексаторы и прочая экзотика

Что  касается  терминологии:  нередко  то,  что  мы  называем  экзем-

пляром  делегата,  называют  делегатом.  В  таком  контексте  то,  что

мы  называем  делегатом,  логично  назвать  типом  делегата.  Иногда

термин делегат используют и непосредственно для делегатов, и для

экземпляров делегатов. Чтобы избежать недоразумений, мы будем

на уровне терминологии разграничивать понятие делегата и экзем-

пляра делегата.

Экземпляр делегата создается по всем правилам ООП-жанра практически

так же, как создаются объекты классов, с той лишь разницей, что вместо

имени класса используется имя делегата. Шаблон создания экземпляра де-

легата (и его инициализации, то есть присваивания значения экземпляру

делегата) может выглядеть так:

имя_делегата экземпляр=new имя_делегата(имя_метода);

Роль аргумента конструктора при этом играет название метода, ссылка на

который присваивается в качестве значения экземпляру делегата. Как де-

легаты объявляются и как создаются экземпляры делегата, иллюстрирует

программный код в листинге 5.6.

Листинг 5.6.  Знакомство с делегатами

using System;

// Объявление делегата GetNum.

// Делегат может ссылаться на метод,

// который возвращает целочисленный

// результат и имеет аргумент - целочисленный

// массив:

delegate int GetNum(int[] arg);

// Класс с двумя методами:

class Nums{

// Метод для вычисления максимального

// числа в массиве:

public int max(int[] m){

int k,s=m[0];

for(k=1;ks) s=m[k];

return s;

}

// Метод для вычисления минимального

// числа в массиве:

public int min(int[] m){

int k,s=m[0];

for(k=1;k

return s;

}

Делегаты           195

}

class DelegateDemo{

// Главный метод программы:

public static void Main(){

// Создание целочисленного массива:

int[] nums={1,-3,5,8,-9,11,-6,15,10,3,-2};

// Создание объекта:

Nums obj=new Nums();

// Создание экземпляра делегата

// и его инициализация:

GetNum FindIt=new GetNum(obj.max);

// Использование экземпляра делегата

// для вызова метода:

Console.WriteLine("Максимальное значение: "+FindIt(nums));

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

// нового значения:

FindIt=obj.min;

// Использование экземпляра делегата

// для вызова метода:

Console.WriteLine("Минимальное значение: "+FindIt(nums));

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

В программе для числового массива вычисляется максимальное и мини-

мальное значения. При этом используется экземпляр делегата. Результат

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

Рис. 5.6.  Знакомство с делегатами —

результат выполнения программы

Инструкция delegate int GetNum(int[] arg) является объявлением де-

легата с именем GetNum. Экземпляр такого делегата может в принципе

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

В классе Nums объявляются два открытых метода (max() и min()). По счаст-

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

целое число. Аргументами методов являются, опять же случайно, цело-

численные массивы. Метод max() возвращает значение наибольшего эле-

196

Глава 5. Свойства, индексаторы и прочая экзотика

мента массива-аргумента, а метод min() возвращает в качестве результата

значение наименьшего элемента массива-аргумента.

В главном методе программы создается числовой массив nums и объект obj класса Nums. Экземпляр делегата GetNum создается и инициализируется с по-

мощью команды GetNum FindIt=new GetNum(obj.max). Экземпляр делегата

называется FindIt, и ссылается этот экземпляр на метод max() объекта obj.

Поэтому в результате выполнения инструкции FindIt(nums) вычисляется

результат выражения obj.max(nams). Командой FindIt=obj.min экземпляру

FindIt делегата GetNum присваивается новое значение. Теперь этот экзем-

пляр ссылается на метод min() объекта obj. Поэтому теперь в результате

выполнения команды FindIt(nums) вычисляется выражение obj.min(nams).

У делегатов для методов, не возвращающих результата, есть одно очень

полезное и интересное свойство: экземпляры таких делегатов могут ссы-

латься сразу на несколько методов. Чтобы добавить имя еще одного метода

в список методов, на которые ссылается экземпляр делегата, текущее зна-

чение экземпляра делегата формально увеличивается на имя метода. Что-

бы не быть голословными, рассмотрим пример в листинге 5.7.

Листинг 5.7.  Ссылка элемента делегата на несколько методов

using System;

// Делегат для метода с аргументом типа double,

// не возвращающем результат:

delegate void MList(double x);

// Класс для вычисления степенной функции:

class Pow{

// Закрытое поле определяет

// целочисленную степень:

private int power;

// Конструктор класса с одним аргументом:

public Pow(int n){

power=n;

}

// Метод с одним аргументом для

// вычисления степени числа

// и отображения результата в консольном окне:

public void GetPower(double x){

double res=1;

// Вычисление степени числа - аргумента метода:

for(int i=1;i<=power;i++){

res*=x;

}

// Отображение результата:

Console.WriteLine("Значение {0} в степени {1}:

{2}",x,power,res);

}

Делегаты           197

}

// Класс с главным методом программы:

class MListDemo{

// Главный метод программы:

public static void Main(){

// Создание экземпляра делегата

// (с пустой ссылкой в качестве значения):

MList ShowItAll=null;

// Формируется значение экземпляра делегата:

for(int i=0;i<=20;i++){

// В список делегата добавляется ссылка

// на новый метод:

ShowItAll+=new Pow(i).GetPower;

}

// Вызываются методы из списка-значения

// экземпляра делегата:

ShowItAll(2);

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

Делегат в этом примере описывается командой delegate void MList (double x). В данном случае область интересов делегата ограничивается

методами с одним аргументом типа double, не возвращающими результат.

Еще мы описали класс с названием Pow, у которого есть целочисленное

закрытое поле, конструктор с одним аргументом (значение аргумента

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

та), а также открытый метод GetPower(), у которого один аргумент типа

double и который не возвращает результат. Методом вычисляется такое

значение: аргумент метода возводится в степень, которая определяется

значением закрытого поля power. Полученное значение, как часть тексто-

вого сообщения, отображается в консольном окне. Сообщение содержит

информацию о том, какое число и в какую степень возводилось и какой

при этом получен результат.

Все самое интересное происходит в главном методе программы. Про-

граммного кода там немного, но код этот очень занимательный. Сначала

командой MList ShowItAll=null мы создаем экземпляр делегата с назва-

нием ShowItAll. В качестве значения экземпляр делегата получает пустую

ссылку (значение null). Такой экземпляр пока что ни на что приличное не

ссылается. Но это пока — точнее, до тех пор, пока не запускается оператор

цикла, в котором индексная переменная i пробегает значения от 0 до 20

включительно. В теле оператора цикла командой ShowItAll+=new Pow(i).

GetPower значение экземпляра делегата «пополняется» ссылкой на метод

GetPower() анонимного объекта, который создается командой new Pow(i).

198

Глава 5. Свойства, индексаторы и прочая экзотика

ПРИМЕЧАНИЕ С анонимными объектами мы уже знакомы. В нашем случае командой

new Pow(i) создается объект класса Pow со значением поля power, равным i. Метод GetPower(), который вызывается из такого объекта, возводит  значение  своего  аргумента  в  степень  i.  Ссылка  на  соот-

ветствующий объект нам особо не нужна — нас интересует ссылка

на метод GetPower() этого объекта. Эту ссылку мы получаем через

инструкцию new Pow(i).GetPower.

Чудо происходит несколько неожиданно — в результате выполнения ко-

манды ShowItAll(2) отображается последовательность сообщений со зна-

чениями целочисленной степени числа 2 (показатель степени меняется

от 0 до 20 — в соответствии с областью изменения индексной переменной

оператора цикла). Результат выполнения программы проиллюстрирован

рис. 5.7.

Рис. 5.7.  Ссылка элемента делегата на несколько методов —

результат выполнения программы

Объяснение у происходящего достаточно простое. При выполнении опера-

тора цикла в главном методе программы к текущему значению экземпляра

делегата ShowItAll последовательно «дописываются» ссылки на методы

GetPower() разных объектов — у каждого следующего объекта значение

поля power на единицу больше, чем у предшественника. Поэтому у каждого

из объектов метод GetPower() вычисляет разные результаты. Когда выпол-

няется команда ShowItAll(2), каждый метод из списка экземпляра деле-

гата ShowItAll вызывается с аргументом 2. Методы из списка вызываются

в том порядке, в котором они в этот список добавлялись.

Знакомство с событиями           199

Метод можно не только добавить в список экземпляра делегата, но

и удалить из списка. Чтобы удалить имя метода из списка-значения

экземпляра делегата, можно использовать оператор -=.

Знакомство с событиями

Я считаю своим долгом поведать наконец,

как все было на самом деле.

Из к/ф «Приключения принца Флоризеля»

Есть категория достаточно экзотических членов класса, которые по ори-

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

Итак, событие — это член класса. Это мы уже знаем. Значением события

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

ность события состоит в том, что оно позволяет выполнить за один заход

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

в списке события. Соответствующий процесс называется генерацией со-

бытия. Вкратце это все. Дальше начинаются подробности.

Событие описывается практически так же, как и обычное поле класса, но

есть два «но»:

 события описываются с ключевым словом event;

 в качестве идентификатора типа события указывается имя делегата, экземпляры которого могут быть значениями события.

ПРИМЕЧАНИЕ Таким образом, при генерировании события могут вызываться только

методы,  соответствующие  определенному  шаблону.  Этот  шаблон

определяется делегатом — типом события.

Для того чтобы сгенерировать событие, необходимо вызвать событие как

обычный метод, с круглыми скобками после имени события и, если не-

обходимо, аргументами. Пикантность ситуации в том, что сгенерировать

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

Генерировать события можно в программном коде их родного класса, но

не за его пределами. При этом методы, на которые ссылаются экземпляры

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

200

Глава 5. Свойства, индексаторы и прочая экзотика

зом, объекты как бы взаимодействуют: событие в одном объекте приводит

к реакции других объектов.

Изменение значения события выполняется с помощью операторов += (до-

бавление экземпляра делегата в список значений события) и ­= (удаление

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

ющие полные формы операторов нельзя. Причина в том, что событие не

возвращает значение, поэтому не может использоваться в выражениях.

Наступил черед примера. Рассмотрим программный код (для консольного

проекта) в листинге. 5.8.

Листинг 5.8.  Знакомство с событиями

using System;

// Делегат для метода с целочисленным аргументом,

// который не возвращает результат:

delegate void NYear(int y);

// Делегат для метода с текстовым аргументом,

// который не возвращает результат:

delegate void Wishes(string w);

// Класс с событиями:

class YearClass{

// Целочисленное поле класса:

public int year;

// Конструктор класса с одним аргументом:

public YearClass(int year){

this.year=year; // Полю присваивается значение

}

// Событие:

public event NYear NewYear;

// Еще одно событие:

public event Wishes GetWishes;

// Метод, в котором генерируются события:

public void StartEvents(string txt){

Console.WriteLine("Первое событие произошло!"); NewYear(year); // Генерируется первое событие

Console.WriteLine("Второе событие произошло!"); GetWishes(txt); // Генерируется второе событие

Console.WriteLine("На сегодня событий больше нет!");

}

}

// Вспомогательный класс:

class Fellow{

// Текстовое поле класса:

public string name;

// Конструктор класса с одним аргументом:

public Fellow(string name){

Знакомство с событиями           201

this.name=name; // Полю присваивается значение

}

// Метод с одним текстовым аргументом.

// Результат метод не возвращает:

public void show(string txt){

Console.WriteLine(name+": "+txt);

}

}

// Класс с главным методом:

class EventDemo{

// Статический метод с целочисленным аргументом.

// Метод не возвращает результат:

public static void show(int year){

Console.WriteLine("Ура! С Новым "+year+" годом!");

}

// Главный метод программы:

public static void Main(){

// Локальная текстовая переменная:

string wishes="С Новым годом!";

// Первый объект вспомогательного класса:

Fellow ivanov=new Fellow("Иван Иванов");

// Второй объект вспомогательного класса:

Fellow petrov=new Fellow("Петр Петров");

// Объект класса с событиями:

YearClass obj=new YearClass(2012);

// Создание экземпляра делегата NYear со ссылкой

// на статический метод show():

NYear eh1=new NYear(show);

// Создание экземпляра делегата Wishes со ссылкой

// на метод show() объекта ivanov:

Wishes eh2=new Wishes(ivanov.show);

// Создание экземпляра делегата Wishes со ссылкой

// на метод show() объекта petrov:

Wishes eh3=new Wishes(petrov.show);

// Определяем значения событий:

obj.NewYear+=eh1;

obj.GetWishes+=eh2;

obj.GetWishes+=eh3;

// Вызываем метод, генерирующий события:

obj.StartEvents(wishes);

// Ожидание нажатия клавиши:

Console.ReadKey();

}

}

Результат выполнения этой программы представлен на рис. 5.8.

202

Глава 5. Свойства, индексаторы и прочая экзотика

Рис. 5.8.  Результат выполнения программы,

содержащей класс с событиями

Кратко проанализируем код. Имеет смысл выделить ключевые моменты.

Так, нами объявлены два делегата. Делегат NYear соответствует методу с це-

лочисленным аргументом и без результата. Делегат Wishes соответствует

методу с текстовым аргументом и тоже без результата. Также мы объявляем

класс YearClass с целочисленным полем year, конструктором и двумя собы-

тиями. Событие NewYear объявлено с типом NYear, поэтому значением собы-

тия могут быть экземпляры этого делегата. Значениями события GetWishes могут быть экземпляры делегата Wishes, поскольку он указан типом собы-

тия. Еще у класса есть метод с текстовым аргументом StartEvents() в кото-

ром командами NewYear(year) и GetWishes(txt) генерируются события.

ПРИМЕЧАНИЕ Команда NewYear(year) означает, что будут последовательно выпол-

нены все методы, зарегистрированные через экземпляры делегата

в событии NewYear. У всех методов будет один и тот же аргумент year.

Аналогично, команда GetWishes(txt) приводит к вызову всех мето-

дов, ссылки на которые есть в экземплярах делегата, присвоенных

в качестве значения события GetWishes.

В программе объявлен вспомогательный класс Fellow, у которого есть тек-

стовое поле, конструктор, и метод show() с текстовым аргументом. Метод

show() не возвращает результат.

В классе EventDemo, помимо главного метода программы, описан статиче-

ский метод show(). У метода целочисленный аргумент и нет результата.

В методе Main() мы создаем текстовую переменную wishes со значением "С Но­

вым годом!", а также создаем два объекта (ivanov и petrov) класса Fellow. Еще

создается объект obj класса YearClass. После этого создается три экземпляра

делегата. Командой NYear eh1=new NYear(show) создается экземпляр делегата

для статического метода show(), а командами Wishes eh2=new Wishes(ivanov.

show) и Wishes eh3=new Wishes(petrov.show) создаются экземпляры делегата

со ссылками на методы show() объектов ivanov и petrov соответственно. Ко-

мандами obj.NewYear+=eh1, obj.GetWishes+=eh2 и obj.GetWishes+=eh3 событи-

ям объекта obj присваиваются значения. События генерируются в результате

выполнения команды obj.StartEvents(wishes).

Элементарная обработка событий           203

Элементарная обработка событий

Надо написать им хорошие песни, и тогда

они перестанут петь плохие.

Из к/ф «Айболит 66»

Здесь мы приподнимем завесу над тайной и перейдем к тому, что един-

ственно возбуждает наше воображение, — к созданию полноценных при-

ложений с графическим интерфейсом. При этом нам понадобится обраба-

тыватьсобытия, которые, как мы уже знаем, являются членами класса.

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

Выше мы так и поступили. Однако там мы сами создавали класс с членами-

событиями. Мы сами писали программный код для генерирования событий

и сами предусматривали механизмы их обработки (реакции на генерирова-

ние событий). Поэтому интриги особой не было — что мы в класс с событи-

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

с графическим интерфейсом и обработке событий в таком приложении, то

ситуация, с одной стороны, вроде аналогичная, но с другой — совершенно

иная. В последнем случае нам предстоит не только иметь дело с событиями-

членами библиотечных классов, но и провести некоторое исследование на

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

жаясь, берутся. Этим, собственно, и займемся. В этом разделе мы будем

обсуждать события, но не сами по себе, а в контексте создания приложения

с функциональным графическим интерфейсом.

ПРИМЕЧАНИЕ Приложение с нефункциональным графическим интерфейсом в виде

чистого окна мы уже создавали.

Проблема усугубляется тем, что есть события — члены класса, а есть со-

бытия в общем филологическом смысле этого слова — когда что-то где-то

происходит. Эти понятия взаимосвязаны, но не тождественны.

Наше бытовое представление о событии несколько отличается от

того, что называется событием в C#. В последнем случае речь идет

о некотором уведомлении, которое получает программа вследствие

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

на  кнопке,  например,  является  действием  пользователя,  а  собы-

тие — это уведомление о том, что соответствующее действие вы-

полнено.

204

Глава 5. Свойства, индексаторы и прочая экзотика

Что же такое «событие» и зачем оно нужно? Рассмотрим на простом при-

мере. Предположим, что у нас есть окно (оконная форма) с кнопкой. Хотя

мы этого еще не знаем, но добавить такую кнопку в форму нет особой

проблемы. Несколько сложнее «внушить» этой кнопке «разумное пове-

дение».

ПРИМЕЧАНИЕ Откровенно говоря, это тоже несложная задача. Другое дело, что

соответствующий несложный код для понимания требует некоторых

нетривиальных разъяснений.

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

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

такой щелчок выполнен. Чего она не знает — это как на щелчок реагиро-

вать. Нам нужно сделать две вещи:

 написать программный код, который будет выполняться при щелчке на

кнопке;

 предпринять необходимые меры, чтобы пометить, что написанный код

выполняется в случае, если произошло событие «щелчок на кнопке».

Нечто похожее мы уже делали в предыдущем разделе. Здесь мы по многим

пунктам будем повторяться, но оправдывает нас важность поставленной

задачи.

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

большому счету, обработчик события — это обычный метод, помеченный

специальным образом так, что он выполняется каждый раз, когда про-

исходит событие. То обстоятельство, что метод является обработчиком

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

за на этот жизненный случай звучит примерно так: «для метода-обра-

бот чика необходимо создать экземпляр делегата и зарегистрировать его

в списке обработчиков события элемента графического интерфейса».

Поскольку эта фраза немного туманная, расшифруем ее — медленно

и подробно.

Элементы графического интерфейса реализуются через объекты специ-

альных библиотечных классов или классов, производных от них. Для та-

ких элементов существует предопределенный набор событий (уведомле-

ний о выполнении определенных действий), которые этот элемент может

сгенерировать. Эти события реализуются в виде членов класса. Все, как

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

заранее, и определены не нами. Это во-первых. Есть и во-вторых: мето-

Элементарная обработка событий           205

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

ся благодаря использованию стандартного делегата EventHandler. Делегат

EventHandler предполагает, что соответствующий метод не возвращает ре-

зультат и у него два аргумента. Первый аргумент — объект класса object.

Этот аргумент определяет объект того компонента, который вызвал собы-

тие.

Как  уже  отмечалось,  класс  object  находится  в  вершине  иерархии

объектной  модели  C#.  Все  классы  для  графических  компонентов

являются потомками этого класса. Идентификатор object является

ссылкой на класс System.Object.

Второй аргумент — объект библиотечного класса EventArgs. Этот объект

содержит описание сгенерированного события. Обычно у нас нет необ-

ходимости использовать ни один из этих аргументов, но все равно метод-

обработчик должен быть описан именно с такими аргументами.

Другими словами, метод, претендующий на почетное звание обработчика

события для элемента графического интерфейса, не должен возвращать

результат и должен иметь два аргумента: объект класса object и объект

класса EventArgs.

Все прочее достаточно стереотипно. После того, как метод создан, объяв-

ляем экземпляр делегата EventHandler и в качестве значения присваиваем

ему ссылку на метод-обработчик. Далее останется только зарегистриро-

вать этот обработчик: с помощью оператора += записываем имя экземпля-

ра делегата в список события-члена объекта графического элемента. Это

самый тонкий момент в наших построениях, поскольку нужно банально

знать, как события называются. Благо, названия у событий достаточно

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

называется. Например, для объектов класса Button (это класс с описанием

кнопки) за щелчок отвечает событие Click.

Теперь от теории переходим к практике. В листинге 5.9 приведен про-

граммный код приложения с графическим интерфейсом — миленьким

окном формы с не менее милой кнопкой. Щелчок на кнопке приводит к за-

крытию окна.

Этот проект в среде Visual C# Express следует реализовать как Windows-

приложение.

206

Глава 5. Свойства, индексаторы и прочая экзотика

Листинг 5.9.  Оконная форма с кнопкой

using System;

using System.Windows.Forms;

// Класс формы создается на основе

// библиотечного класса Form:

class MyForm:Form{

// Ссылка на кнопку - закрытое поле класса формы:

private Button btn;

// Конструктор класса с текстовым аргументом:

public MyForm(string txt){

// Параметры окна формы:

Text=txt; // Заголовок окна

Height=200; // Высота окна формы

Width=300; // Ширина окна формы

// Создание объекта кнопки:

btn=new Button();

// Параметры кнопки:

btn.Text="OK"; // Текст кнопки

btn.Height=25; // Высота кнопки

btn.Width=50; // Ширина кнопки

btn.Top=125; // Координата левого верхнего угла

// кнопки по вертикали

btn.Left=125; // Координата левого верхнего угла

// кнопки по горизонтали

// Создается экземпляр делегата EventHandler.

// Значение экземпляра - ссылка на метод CloseAll():

EventHandler handler=CloseAll;

// Регистрация обработчика события

// щелчка на кнопке.

// Событие Click кнопки "увеличивается" на handler: btn.Click+=handler;

// Добавление кнопки в форму:

Controls.Add(btn);

}

// Закрытый метод для обработки щелчка

// на кнопке формы:

private void CloseAll(object obj,EventArgs args){

// Завершается работа приложения:

Application.Exit();

}

}

// Класс с главным методом программы:

class OneButtonDemo{

// Инструкция выполнять приложение

// в едином потоке:

[STAThread]

Элементарная обработка событий           207

// Главный метод программы:

public static void Main(){

// Отображение оконной формы с кнопкой:

Application.Run(new MyForm("Окно с кнопкой"));

}

}

Чтобы анализ программного кода был более простым, сразу отметим, что

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

и неприметное окно, показанное на рис. 5.9.

Рис. 5.9.  Оконная форма с кнопкой отображается

в результате выполнения программы

Однако это на первый взгляд непримечательное окно является большим

шагом вперед, поскольку оно не просто содержит кнопку с банальным

названием OK, но эта кнопка еще и функционирует — если щелкнуть на

ней, окно будет закрыто, а работа приложения завершена. Это открыва-

ет поистине широкие перспективы. А теперь вернемся к программному

коду.

Некоторые действия нам уже знакомы по предыдущим главам. Тем не ме-

нее освежить память совсем не помешает. Итак, для реализации формы

с кнопкой создаем класс MyForm, но создаем не на ровном месте: класс на-

следует библиотечный класс Form. У класса MyForm имеется одно закрытое

поле btn. Это объектная переменная класса Button. Именно класс Button будет использован для создания кнопки. Но это будет происходить в кон-

структоре класса.

Общая схема добавления элемента графического интерфейса в фор-

му, в том числе и кнопки, подразумевает, во-первых, создание соот-

ветствующего объекта и, во-вторых, «связывания» (или добавления) этого элемента с формой. Для добавления элемента в форму исполь-

зуется метод Add(). Метод вызывается из свойства Controls, которое

представляет собой коллекцию элементов формы.

208

Глава 5. Свойства, индексаторы и прочая экзотика

У конструктора класса MyForm есть текстовый аргумент. Этот тестовый ар-

гумент определяет название окна формы (отображается в поле названия

окна). За название окна формы отвечает свойство Text. Свойства Height и Width определяют, соответственно, высоту и ширину окна формы. Ука-

занным трем свойствам формы в конструкторе присваиваются значения.

Но это не главное. В конструкторе командой btn=new Button() создается

объект кнопки. У кнопки имеются свойства с такими же названиями, что

и перечисленные выше свойства формы. Обращение к свойствам кнопки

выполняется с указанием объекта btn этой кнопки. Например, свойство

btn.Text определяет текст, отображаемый на кнопке, реализованной че-

рез объект btn. Свойство btn.Height определяет высоту кнопки, а свой-

ство btn.Width определяет ширину кнопки. Есть еще два полезных свой-

ства кнопки, которые задаются в конструкторе. Это свойства Top и Left.

Первое задает вертикальную координату левого верхнего угла кнопки, а второе задает горизонтальную координату левого верхнего угла кнопки.

Таким образом, задав эти свойства, можно определить положение кнопки

в окне формы.

Координаты определяются в поинтах по отношению к левому верх-

нему углу формы. Горизонтальная координата отсчитывается вправо, а вертикальная — вниз.

На этом все основные внешние параметры кнопки определены. Но есть

еще два момента:

 Кнопку нужно «оживить», добавив обработчик события щелчка на

кнопке.

 Кнопку нужно добавить в форму.

Создание объекта кнопки не означает, что кнопка добавлена в форму.

Предпосылки для «оживления» кнопки появляются благодаря команде

EventHandler handler=CloseAll. Командой объявляется экземпляр handler делегата EventHandler. В качестве значения экземпляру делегата присваи-

вается ссылка на метод CloseAll(). Этот метод мы обсудим позже. Сейчас

нам важно, что именно на этот метод ссылается экземпляр handler делегата

EventHandler. Командой btn.Click+=handler выполняется регистрация об-

работчика события щелчка на кнопке. Выглядит это примерно так: собы-

тию Click, которое в кнопке btn отвечает за действие «щелчок на кнопке»

присваивается экземпляр делегата handler, который, в свою очередь, ссы-

лается на метод ClaseAll(). При генерации события «щелчок на кнопке»

Элементарная обработка событий           209

будут выполнены те методы, на которые ссылаются экземпляры делегатов, записанные в событии Click. В данном случае это метод CloseAll().

Строго говоря, команда вида btn.Click+=handler формально выглядит

так, будто событие Click «увеличивается» на значение handler. На

практике это означает следующее. В общем случае значением со-

бытия Click является список из экземпляров делегатов. «Увеличение»

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

делегата дописывается в список-значение события Click. При возник-

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

для которых представлены в событии.

Из сказанного следует, что у одного события может быт несколько

обработчиков. Также следует иметь в виду, что при желании экзем-

пляр делегата можно удалить из списка-значения события Click. Для

этого используют оператор -=. Добавление/удаление экземпляров

делегатов выполняется только сокращенными формами оператора

присваивания (соответственно, += и -=). Разумеется, все вышеска-

занное относится и к прочим событиям, связанным с графическими

элементами.

Финальным кульминационным штрихом программного кода конструк-

тора является команда Controls.Add(btn), которой кнопка добавляется

в форму.

Как уже отмечалось выше, в классе Form есть свойство Controls, кото-

рое представляет собой коллекцию тех объектов, которые включены в

форму. Поэтому, чтобы включить новый компонент в форму, объекту

этого элемента необходимо «отметиться» в свойстве Controls. Специ-

ально для этих целей у свойства есть метод Add(). Объект добавляе-

мого в форму компонента указывается аргументом метода.

Для анализа кода класса MyClass осталось проанализировать программ-

ный код закрытого метода CloseAll(), который выполняется при щелчке

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

ра и тип результата метода должны соответствовать делегату EventHandler.

В теле метода выполняется всего одна команда — Application.Exit(), ко-

торой завершается работа приложения.

В главном методе программы форма отображается командой Application.

Run(new MyForm("Окно с кнопкой")). Аргументом метода Run() указан ано-

нимный объект класса MyForm.

210

Глава 5. Свойства, индексаторы и прочая экзотика

ПРИМЕЧАНИЕ Для запуска приложения и завершения его работы мы используем

методы класса Application.

Более подробно методы создания приложений с графическим интерфей-

сом обсуждаются в последней главе книги, которая содержит достаточно

большой учебный пример.

Важные конструкции

Ходы кривые роет подземный умный крот.

Нормальные герои всегда идут в обход.

Из к/ф «Айболит 66»

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

остановиться нельзя, а раньше такой возможности не было. У нас достаточ-

но красочная и нетривиальная культурная программа. Мы познакомимся

с абстрактными классами и интерфейсами, структурами и перечисления-

ми. План вроде бы небольшой, но довольно содержательный.

Перечисления

Мы продолжаем то, что мы уже много наделали.

В. Черномырдин

В качестве разминки познакомимся с перечислениями. Это и несложно, и полезно — мы с ними уже встречались, да и в дальнейшем нам они еще

понадобятся. Перечисление в C# — это набор постоянных значений, кото-

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

речисления выполняется с помощью ключевого слова enum, после которого

212

Глава 6. Важные конструкции

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

enum имя_перечисления{константа1,константа2,...,константаN}

В списке значений перечисления указываются имена целочисленных

констант. По умолчанию эти константы получают значения: 0 — первая

константа в списке, 1 — вторая константа в списке, и т. д. При желании

некоторым или всем константам можно присвоить уникальные значения.

Правило такое: если явно значение константы в списке не указано, то ее

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

Тип данных, который лежит в основе перечисления, называется основ-

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

может быть целочисленный тип: byte, sbyte, short, ushort, int, uint, long или ulong. По умолчанию используется тип int. При желании

базовый  тип  перечисления  можно  указать  через  двоеточие  после

имени перечисления. Например, так: enum days:sbyte{Sun,Mon,Tue, Wed,Thu,Fri,Sat}.

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

имя перечисления и, через точку, имя константы из списка значений: то

есть в формате имя_переичсления.константа. В листинге 6.1 приведен при-

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

Листинг 6.1.  Знакомство с перечислениями

using System;

class EnumDemo{

// Перечисление colors:

enum colors{red,green,blue,yellow,white};

// Перечисление numbers:

enum numbers{first=100,second,third,fourth,fifth};

// Главный метод программы:

public static void Main(){

// Объявление переменной типа colors:

colors cls;

// Объявление и инициализация переменной

// типа numbers:

numbers nms=numbers.first;

// В операторе цикла индексная переменная

// типа colors:

for(cls=colors.red;cls<=colors.white;cls++){

// Используем переменную типа colors:

Console.WriteLine(cls+" - числовое значение "+(int)cls);

}

Перечисления           213

Console.WriteLine(); // Новая строка

// В операторе цикла используется

// переменная-счетчик типа numbers:

while(nms<=numbers.fifth){

// Используем переменную типа letters:

Console.WriteLine(nms+" - числовое значение "+(int)nms); nms++;

}

Console.ReadLine();

}

}

Интерес в этом программном коде представляют команды, которыми

объявляются перечисления. Их две. Перечисление colors объявляется

командой enum colors{red,green,blue,yellow,white}, а перечисление

numbers объявляется командой enum numbers{first=100,second,third, fourth,fifth}. Принципиальное различие в том, что во втором случае

для первого элемента явно указано базовое числовое значение. Объяв-

ляются переменные типа перечисления так же, как и переменны прочих

типов, — указывается имя перечисления и имя переменной. Если пере-

менной типа перечисления присваивается значение, то перед соответ-

ствующей константой (через точку) нужно указать имя перечисления —

как, например, в команде nms=numbers.first. Также примечателен тот

факт, что по отношению к переменным типа перечисления применимы

операции инкремента/декремента. Результат выполнения программы

проиллюстрирован на рис. 6.1.

Рис. 6.1.  Знакомство с перечислениями —

результат выполнения программы

Неявное преобразование значения типа перечисления к числовому

типу не выполняется, поэтому, если мы хотим узнать базовое числовое

значение переменной, используем инструкцию явного приведения

типа.

214

Глава 6. Важные конструкции

Знакомство со структурами

Ничего, ослы даже лучше, чем дикие

скакуны. Они не будут умничать!

Из к/ф «Айболит 66»

Структуры в известном смысле могут рассматриваться как альтернатива

классам — правда, не такая функциональная, зато более быстрая. Описы-

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

Если коротко, то объявление структуры отличается от объявления класса

заменой ключевого слова class на struct. После ключевого слова struct указывается имя структуры и, в фигурных скобках, описываются поля

и методы структуры. То есть шаблон объявления структуры такой: struct имя_структуры{

// Поля и методы структуры

}

Поля и методы структуры описываются так же, как поля и методы класса.

В этом смысле сходство достаточно большое.

Членами структуры могут быть также свойства, индексаторы и опе-

раторные методы.

Естественным образом закрадывается сомнение: а нужны ли вообще

структуры, если у нас есть такое чудо современной программной мысли, как класс? Чтобы было легче отвечать на этот простой вопрос, мы его не-

сколько переформулируем: что такого есть в структурах, что позволяет им

выжить в ООП? Главное преимущество, которое не дает потерять лицо

на фоне могущества классов, состоит в том, что структуры, в отличие от

классов, реализуются как тип с прямым доступом. Доступ к классам, как

мы помним, осуществляется через объектные переменные, то есть доступ

к объекту выполняется через ссылку. Поэтому про объекты говорят, что

они относятся к ссылочным типам. На ситуацию можно посмотреть и ина-

че, в том контексте, что при работе с объектами мы оперируем объектными

переменными, которые объектами не являются. В случае со структурами

ситуация иная. Создавая переменную типа структуры (или структурную

переменную — аналог объекта класса, — которую будем называть экзем-

пляром структуры), мы не задействуем никаких «посредников». Струк-

турная переменная — это и есть экземпляр структуры. А теперь зададимся

Знакомство со структурами           215

вопросом: в каком случае операции выполняются быстрее — при наличии

«посредников» или без них? Ответ, думается, очевиден.

Хотя на стороне структур есть такое серьезное преимущество, как прямой

доступ, у них есть и серьезные недостатки (хотя, конечно, как посмотреть).

Например:

 Для структур нет наследования: структуры не могут наследовать струк-

туры или классы, а классы не могут наследовать структуры.

Вместе с тем структуры могут реализовать интерфейсы, о которых

рассказывается  далее.  Имя  реализуемого  в  структуре  интерфейса

указывается после имени структуры через двоеточие. Если реализуе-

мых интерфейсов несколько (а это допустимо), их имена разделяются

запятыми.

 У структур есть конструкторы, но нет деструкторов. Конструктор без

аргументов не может быть переопределен — по умолчанию есть только

один непереопределяемый конструктор без аргументов.

 У структур нет защищенных членов (protected-членов) — в них просто

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

 Создавать экземпляр структуры можно простым объявлением струк-

турной переменной. При этом экземпляр структуры создается, но не

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

Тем не менее можно экземпляры структуры создавать и с помощью

оператора new.

 Копирование структур выполняется так же, как и переменных базовых

типов, то есть в побитовом режиме.

В нашем нелегком деле изучения языка программирования C# структу-

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

знакомство с ними ограничим простым примером, приведенным в листин-

ге 6.2.

Листинг 6.2.  Знакомство со структурами

using System;

// Структура для реализации комплексных чисел:

struct SCompl{

// Закрытое поле - действительная

// часть комплексного числа:

private double Re;

продолжение

216

Глава 6. Важные конструкции

Листинг 6.2 (продолжение)

// Закрытое поле - мнимая часть

// комплексного числа:

private double Im;

// Конструктор с двумя аргументами:

public SCompl(double Re,double Im){

this.Re=Re;

this.Im=Im;

}

// Свойство для вычисления модуля

// комплексного числа:

public double mod{

get{ // Аксессор для считывания значения свойства

return Math.Sqrt(Re*Re+Im*Im);

}

}

// Метод для отображения значения полей:

public void show(){

Console.WriteLine("Число: Re={0}, Im={1};",Re,Im);

}

// Метод для присваивания значения полям:

public void set(double Re,double Im){

this.Re=Re;

this.Im=Im;

}

// Перегрузка оператора сложения:

public static SCompl operator+(SCompl a,SCompl b){

// Результат сложения комплексных чисел:

return new SCompl(a.Re+b.Re,a.Im+b.Im);

}

}

// Класс с главным методом программы:

class StructDemo{

// Главный метод программы:

public static void Main(){

// Создание экземпляра структуры:

SCompl a=new SCompl(1,-2);

// Объявление структурных переменных:

SCompl b,c;

// Присваивание экземпляров структур:

b=a;

// Изменение значений полей

// экземпляра структуры:

a.set(2,6);

// Вычисление суммы двух экземпляров структуры:

c=a+b;

Знакомство со структурами           217

// Вызов метода из экземпляра структуры:

c.show();

// Обращение к свойству экземпляра структуры:

Console.WriteLine("Модуль числа: {0}.",c.mod);

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

В представленной программе мы попытались с помощью структуры

SCompl создать небольшую утилиту для работы с комплексными числами.

У структуры имеется два закрытых поля, Re и Im, типа double, которые как

бы олицетворяют собой главные признаки комплексного числа — его дей-

ствительную и мнимую части. Для структуры описан конструктор с двумя

аргументами. Метод show() предназначен для отображения значений по-

лей структуры, а метод set() позволяет этим самым полям присваивать

значения. Свойство mod имеет только get-аксессор, результатом которого

возвращается модуль комплексного числа (вычисляется как корень ква-

дратный из суммы квадратов действительной и мнимой частей комплекс-

ного числа). Еще в структуре перегружается оператор сложения так, чтобы

можно было складывать два экземпляра структур в соответствии с прави-

лами сложения комплексных чисел. Собственно, и все. В главном методе

программы проверяется работоспособность созданной структуры. Резуль-

тат выполнения этой программы представлен на рис. 6.2.

Рис. 6.2.  Знакомство со структурами — результат выполнения программы

Есть несколько моментов, на которые стоит обратить внимание. В основ-

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

и экземпляры структур используются на практике.

Обратите внимание на то, что ключевое слово this в программном

коде структуры используется как ссылка на экземпляр структуры, из

которого  вызывается  метод/конструктор.  Вообще,  для  понимания

того,  как  «функционирует»  код  со  структурами  достаточно  часто

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

объекта этого класса.

218

Глава 6. Важные конструкции

Командой SCompl a=new SCompl(1,-2) экземпляр структуры создается фор-

мально так же, как создается объект класса. Экземпляр структуры a соот-

ветствует числу 1 – 2 i . Как следствие выполнения команды SCompl b,c объявляются и создаются еще два экземпляра структуры, однако они не

инициализированы (полям не присвоены значения). Командой b=a выпол-

няется копирование экземпляров структур. После этого экземпляр струк-

туры b также соответствует числу 1 – 2 i, но «технически» экземпляры a и b разные (то есть это два разных экземпляра структуры с одинаковыми

значениями полей). Поэтому после выполнения команды a.set(2,6) пере-

менная a соответствует числу 1 – 6 i, а переменная b своего значения не

меняет. В результате выполнения команды c=a+b, которая корректна бла-

годаря перегруженному оператору сложения, переменная c соответствует

комплексному числу 3 – 4 i, что и подтверждается результатом выполнения

команд c.show() и Console.WriteLine("Модуль числа: {0}.",c.mod).

Абстрактные классы

— Пойдем в обход!

— Зачем? Он же вот он!

— Тихо! В обход!

Из к/ф «Айболит 66»

Есть один прием, который позволяет придать значимости лектору, доклад-

чику или, на худой конец, автору книги. Состоит он в том, чтобы сначала

все запутать до невозможности, а потом с видом всезнайки, разложить все

по полочкам. Так вот, абстрактный класс — это класс, в котором есть хотя

бы один абстрактный метод. Осталось разобраться, что это такое. И здесь

как раз все более-менее просто. Абстрактный метод — это метод, у кото-

рого есть заголовок (указан тип возвращаемого результата и сигнатура), но нет основного тела. Другим словами, абстрактный метод, это как бы не

до конца описанный метод — не содержащий программного кода, в кото-

ром определяются команды, выполняемые при вызове метода. Уже из ска-

занного становится очевидным, что абстрактный метод сам по себе может

представлять интерес тоже достаточно абстрактный. Объяснение простое

и очевидное. Если программный код метода не определен, а заданы толь-

ко общие параметры метода (имя метода, тип возвращаемого результата

и список аргументов), то вызывать этот метод в программном коде смысла

нет. Зачем же тогда нужны абстрактные методы? А нужны они для того, чтобы делать более гибким программный код, в котором используется на-

следование. При наследовании в производном классе для абстрактного

Абстрактные классы           219

метода из базового класса «доопределяется» программный код, и все ста-

новится так, как и должно быть — у метода есть программный код, и этот

метод можно вызывать.

Напоминаем, что ситуация, когда унаследованный из базового класса

метод  заменяется  методом  с  такой  же  сигнатурой  в  производном

классе, называется переопределением метода. При переопределе-

нии метода в производном классе используют атрибут override. Это

же относится и к абстрактным методам, которые «доопределяются»

в производном классе.

Мы рассмотрим пример подобной ситуации, но прежде уделим немного

внимания формальным вещам: как описываются абстрактные методы и со-

держащие их абстрактные классы.

Итак, чтобы метод стал абстрактным, необходимо, во-первых, описать его

без основного тела с программным кодом и, во-вторых, в заголовке мето-

да использовать атрибут abstract. Абстрактный класс также описывается

с этим атрибутом. Вот, собственно, и все. Теперь настал черед примера. Об-

ратимся к листингу 6.3.

ПРИМЕЧАНИЕ Пример простой, но полезный. В нем мы создаем базовый абстракт-

ный класс, в котором задаем все основные параметры оконной формы

с кнопкой и текстовой меткой. Текстовая метка, как несложно дога-

даться, позволяет отображать текст, а кнопка позволяет выполнять

некоторые  действия.  Метод,  который  фактически  вызывается  при

щелчке  на  кнопке,  объявлен  как  абстрактный.  Это  позволяет  нам

создавать на основе абстрактного класса производные классы. Пере-

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

Листинг 6.3.  Знакомство с абстрактными классами и методами

using System;

using System.Drawing;

using System.Windows.Forms;

// Абстрактный класс:

abstract class MyForm:Form{ // Производный класс от класса Form

// Поле - ссылка на кнопку:

protected Button btn;

продолжение

220

Глава 6. Важные конструкции

Листинг 6.3 (продолжение)

// Поле - ссылка на текстовую метку:

protected Label lbl;

// Конструктор класса с текстовым аргументом:

public MyForm(string txt){

// Высота окна формы:

Height=200;

// Ширина окна формы:

Width=300;

// Тип границ формы - фиксированный размер:

FormBorderStyle=FormBorderStyle.FixedDialog;

// Создание объекта кнопки:

btn=new Button();

// Название для кнопки:

btn.Text="OK";

// Высота кнопки:

btn.Height=(int)(0.15*Height);

// Ширина кнопки:

btn.Width=Width/3;

// Определяем положение кнопки в окне формы:

btn.Location=new Point((Width-btn.Width)/2,(int)

(0.8*Height)-btn.Height);

// Делегат для обработчика события для кнопки:

EventHandler eh=new EventHandler(ButtonHandler);

// Регистрация обработчика щелчка на кнопке:

btn.Click+=eh;

// Добавление кнопки в форму:

Controls.Add(btn);

// Создание объекта текстовой метки:

lbl=new Label();

// Высота области метки:

lbl.Height=Height/2;

// Ширина области метки:

lbl.Width=(int)(0.8*Width);

// Расстояние до левого верхнего угла области

// формы по горизонтали:

lbl.Left=(int)(0.1*Width);

// Расстояние до левого верхнего угла области

// формы по вертикали:

lbl.Top=(int)(0.1*Height);

// Текст метки:

lbl.Text=txt;

// Выравнивание текста в метке:

lbl.TextAlign=ContentAlignment.MiddleCenter;

// Определяем шрифт для отображения

// текста метки:

Абстрактные классы           221

lbl.Font=new Font("Courier New",14);

// Трехмерная граница области текстовой метки:

lbl.BorderStyle=BorderStyle.Fixed3D;

// Добавление метки в форму:

Controls.Add(lbl);

}

// Закрытый метод - обработчик события щелчка

// на кнопке:

private void ButtonHandler(Object obj,EventArgs ea){

// Вызывается еще один метод - абстрактный:

WhatToDo();

}

// Абстрактный метод, который выполняется

// при щелчке на кнопке:

protected abstract void WhatToDo();

}

// Производный класс от абстрактного класса MyForm:

class SimpleForm:MyForm{

// Конструктор класса с текстовым аргументом:

public SimpleForm(string txt):base(txt){

// Заголовок окна:

Text="Еще одно окно";

}

// Переопределение метода, выполняемого

// при щелчке на кнопке:

protected override void WhatToDo(){

// Завершается работа приложения:

Application.Exit();

}

}

// Еще один производный класс от класса MyForm:

class NewForm:MyForm{

// Конструктор класса с текстовым аргументом:

public NewForm(string txt):base(txt){

// Заголовок окна формы:

Text="Новое окно";

}

// Переопределение метода, который выполняется

// при щелчке на кнопке:

protected override void WhatToDo(){

// Окно формы убирается с экрана:

Hide();

// Создается объект новой формы:

SimpleForm sform=new SimpleForm("Сообщение во втором окне"); продолжение

222

Глава 6. Важные конструкции

Листинг 6.3 (продолжение)

// Отображаем окно новой формы:

sform.Show();

}

}

// Класс с главным методом программы:

class AbstractClassDemo{

// Инструкция выполнять программу

// в едином потоке:

[STAThread]

// Главный метод программы:

public static void Main(){

// Создание объекта формы:

NewForm nform=new NewForm("Сообщение в первом окне");

// Отображение формы:

Application.Run(nform);

}

}

Заголовок объявляемого в начале программного кода абстрактного класса

MyForm имеет вид abstract class MyForm:Form. Из анализа этого заголовка

можно сделать два вывода: класс абстрактный, и класс создается на основе

библиотечного класса Form. Это уже само по себе о многом говорит.

ПРИМЕЧАНИЕ Как минимум, это говорит о том, что мы будем иметь дело с графиче-

скими окнами, и созданием одного класса дело не ограничится.

У создаваемого нами класса есть два защищенных поля: поле Button btn является ссылкой на кнопку, а поле Label lbl является ссылкой на тексто-

вую метку.

Текстовая метка — объект библиотечного класса Label. Соответствен-

но, объектная переменная для метки объявляется как такая, которая

относится к классу Label. Что касается самих меток, то их основное

предназначение — отображать текст. Именно с этой целью мы будем

использовать текстовую метку в форме.

Эти два объекта будут предметом нашего пристального внимания. Для

начала их нужно создать, настроить и «закрепить» на форме. Все эти

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

вый аргумент (объявлен как string txt). В конструкторе командами

Height=200 и Width=300 задается высота и ширина формы, а командой

FormBorderStyle=FormBorderStyle.FixedDialog определяется тип границы

Абстрактные классы           223

формы. В данном случае это форма неизменяемого размера, как в «класси-

ческих» диалоговых окнах.

За тип границы формы отвечает свойство FormBorderStyle. В каче-

стве значения этому свойству присваивается константа FixedDialog, которая входит в перечисление FormBorderStyle.

После этого командой btn=new Button() мы создаем объект для кнопки

и начинаем его «настраивать». Текст кнопки определяется командой btn.

Text="OK". Высота и ширина кнопки определяются в пропорции к раз-

мерам окна формы командами btn.Height=(int)(0.15*Height) и btn.

Width=Width/3. Инструкцию явного приведения типа мы использовали для

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

действительного литерала на целое число.

ПРИМЕЧАНИЕ При вычислении значения Width/3 выполняется деление двух целых

чисел, и такая операция, напомним, по умолчанию выполняется, как

деление нацело. Поэтому здесь в явном приведении типа необхо-

димости нет.

Положение кнопки на форме мы задаем с помощью свойства Location кнопки. Этому свойству в качестве значения присваивается вновь создан-

ный экземпляр структуры Point.

Со  структурами  мы  познакомились  выше.  Еще  раз  напомним,  что

структуры во многом напоминают классы. У них, как и у классов, есть

конструкторы. То, что для класса называется объектом, для структуры

мы называем экземпляром структуры.

Аргументами конструктора указываются горизонтальная и вертикальная

координаты левого верхнего угла кнопки. Соответствующая команда име-

ет вид btn.Location=new Point((Width-btn.Width)/2,(int)(0.8*Height)-

btn.Height). Вычисление координат кнопки выполняются с помощью па-

раметров высоты и ширины формы и кнопки.

По  горизонтали  кнопка  отображается  по  центру.  Из  соображений

симметрии очевидно, что горизонтальная координата равна половине

разности ширины формы и ширины кнопки. Координата по вертикали

вычисляется так: 80% высоты формы минус высота кнопки.

224

Глава 6. Важные конструкции

Также для кнопки регистрируется обработчик для события щелчка

на кнопке. С этой целью командой EventHandler eh=new EventHandler (ButtonHandler) создается экземпляр делегата eh для обработчика собы-

тия. Экземпляр делегата ссылается на метод ButtonHandler() (об этом

методе мы еще поговорим). Регистрация обработчика щелчка на кнопке

выполняется командой btn.Click+=eh. Наконец, кнопку в форму добав-

ляем командой Controls.Add(btn). После этого приступаем к созданию

текстовой метки.

Объект метки создается простой и понятной командой lbl=new Label().

Размер метки — это размер той области, в которой отображается текст.

Понятно, что область должна быть достаточно большой, чтобы текст

там поместился. Обычно рамки области метки не отображают. Мы по-

ступим иначе — исключительно для того, чтобы читатель имел более

наглядное представление о размерах и положении метки. Существуют

разные способы добиться нужного результата. Мы воспользуемся од-

ним из них.

У метки есть набор свойств, схожих по названию со свойствами кнопки, которые позволяют задать геометрические размеры области метки, ее по-

ложение и ряд других свойств. Так, командой lbl.Height=Height/2 высо-

та метки задается равной половине высоты окна формы. Ширина области

метки составляет 80% ширины окна формы (команда lbl.Width=(int) (0.8*Width)). Расстояние до левого верхнего угла области формы по гори-

зонтали определяется командой lbl.Left=(int)(0.1*Width), а расстояние

до левого верхнего угла области формы по вертикали определяем коман-

дой lbl.Top=(int)(0.1*Height). Свойство Text метки определяет тот текст, который будет отображаться в метке. В данном случае в области метки мы

будем отображать тот текст, который передается конструктору класса (пе-

ременная txt). Поэтому имеет место команда lbl.Text=txt. Кроме этого, мы хотим явно задать способ выравнивания текста в метке. С этой целью

мы использовали команду lbl.TextAlign=ContentAlignment.MiddleCenter, которой свойству TextAlign присвоили в качестве значения константу

MiddleCenter из перечисления ContentAlignment. Константа MiddleCenter в качестве значения свойства TextAlign означает, что текст будет выравни-

ваться по центру — как по высоте, так и по ширине.

К свойствам формы мы обращаемся по имени, в то время как к одно-

именным свойствам кнопки и метки обращение выполняется с ука-

занием имени объекта. Например, Height означает свойство формы, а btn.Height означает свойство кнопки. На самом деле Height — это

сокращенная форма ссылки this.Height, где в данном контексте this обозначает объект формы.

Абстрактные классы           225

Для объектов с текстом можно задавать шрифт, который применяется при

отображении текста. Свойства шрифта определяются объектом специаль-

ного класса Font. Объект класса Font с настройками шрифта присваивает-

ся в качестве значения свойству Font объекта, для которого выполняется

такая настройка, — в данном случае речь идет об объекте кнопки. Мы ис-

пользуем команду lbl.Font=new Font("Courier New",14). В этой команде

аргументами конструктору класса Font передается текстовая строка с име-

нем шрифта и числовое значение, определяющее его размер.

Как отмечалось выше, не корысти ради, но исключительно в учебных

целях, мы выделяем границу области метки. Для этого свойству метки

BorderStyle присваиваем в качестве значения константу Fixed3D (трехмер-

ная граница) из перечисления BorderStyle. Все это нам обеспечивает ко-

манда lbl.BorderStyle=BorderStyle.Fixed3D. Чтобы добавить метку в фор-

му, используем команду Controls.Add(lbl).

На этом код конструктора класса исчерпан, и мы приступаем к анализу

методов, которые определяют функциональность оконной формы, а точ-

нее, реакцию на щелчок кнопки. Ранее в качестве обработчика щелчка на

кнопке регистрировался экземпляр делегата, содержащий ссылку на метод

ButtonHandler(). Метод описан как закрытый, он не возвращает результат, и у него два аргумента (объекты класса Object и EventArgs). В теле мето-

да вызывается другой метод — это абстрактный метод WhatToDo(). Метод

WhatToDo() описан командой protected abstract void WhatToDo(). Он не

имеет аргументов и не возвращает результат. Но самое главное — он аб-

страктный. Поэтому для класса MyForm нельзя создать объект, но его можно

наследовать. И при наследовании необходимо определить код абстрактно-

го метода WhatToDo().

Мы воспользовались тем, что при обработке щелчка на кнопке ар-

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

ButtonHandler() «вложили» вызов другого метода, абстрактного, ко-

торому аргументы не нужны. Переопределяя этот абстрактный метод

для разных классов-наследников класса MyForm, мы можем создавать

различные типы оконных форм.

На основе класса MyForm путем наследования создается два новых класса.

Класс SimpleForm содержит описание конструктора с одним текстовым

аргументом (переменная txt), который, благодаря инструкции base(txt), передается в конструктор базового класса и таким образом определяет

текст метки. Кроме того, в конструкторе командой Text="Еще одно окно"

задается заголовок окна формы. Таким образом, все окна, которые мы

226

Глава 6. Важные конструкции

будем создавать на основе класса SimpleForm, будут иметь заголовок

Еще одно окно. Но нас, разумеется, интересует переопределение метода

WhatToDo(). В классе SimpleForm мы переопределяем метод с заголовком

protected override void WhatToDo(). В теле метода всего одна команда

Application.Exit(), выполнение которой приводит к завершению работы

приложения.

Класс NewForm также является производным от абстрактного класса MyForm.

У конструктора класса текстовый аргумент, который определяет текст

метки. Свойству Text формы в конструкторе присваивается значение "Но­

вое окно". Как результат, все окна, созданные на основе этого класса, имеют

соответствующий заголовок. При переопределении метода WhatToDo() в теле

метода выполняются три команды, с помощью которых закрывается одна

форма, и открывается другая. Командой Hide() форма убирается с экрана

(но не выгружается из памяти — то есть она существует, но ее не видно). По-

сле этого командой SimpleForm sform=new SimpleForm("Сообщение во вто­

ром окне") создается объект sform для формы класса SimpleForm с текстом

"Сообщение во втором окне" в текстовой метке. Для отображения окна этой

формы из объекта формы вызываем метод Show(). Вся команда выглядит

как sform.Show().

Осталось только проверить, как все это работает. В главном методе про-

граммы командой NewForm nform=new NewForm("Сообщение в первом окне") создаем объект класса NewForm, после чего командой Application.Run(nform) отображаем эту форму на экране.

Мы  отображаем  форму  как  вызовом  метода  Application.Run() с  аргументом-ссылкой  на  объект  формы,  так  и  с  помощью  метода

Show(), вызываемого из объекта формы. Это далеко не одно и то же.

Если закрыть форму, «запущенную» методом Run(), будут закрыты

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

ствий» такая. При выполнении программы запускается метод Main().

Как только дело доходит до выполнения метода Application.Run(), он

забирает на себя управление, и вернет это управление методу Main() при закрытии соответствующей формы или вследствие выполнения

метода  Application.Exit().  Поэтому,  собственно,  мы  первую  форму

(она  открывается  методом  Application.Run())  в  нашем  проекте  не

закрываем, а всего лишь убираем с экрана.

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

ставленное на рис. 6.3.

После щелчка на кнопке OK это окно закрывается, а вместо него появляется

другое окно, которое можно наблюдать на рис. 6.4.

Интерфейсы           227

Рис. 6.3.  Первое окно, которое отображается

в результате выполнения программы

Рис. 6.4.  Второе окно, которое отображается

в результате выполнения программы

А вот если щелкнуть на кнопке OK в этом, втором, окне, приложение завер-

шит свою работу.

В начальной части программного кода появилась относительно новая

для нас инструкция подключения пространства имен using System.

Drawing. Без подключения этого пространства имен часть инструкций

будет непонятна компилятору — в частности, инструкция создания

объекта класса Point.

Интерфейсы

Наше повеление: этот танец не вяжется

с королевской честью, мы запрещаем его

на веки веков!

Из к/ф «31 июня»

Кульминацией развития теории и практики абстрактных классов явля-

ется концепция интерфейсов. Интерфейс — это набор из исключительно

228

Глава 6. Важные конструкции

абстрактных методов, к которым по необходимости могут примкнуть свой-

ства и индикаторы с объявленными, но не описанными, аксессорами. Объ-

является интерфейс достаточно просто: после ключевого слова interface указывается имя интерфейса и, в фигурных скобках, список методов, свойств и индексаторов:

interface имя{

// Так в интерфейсе объявляется метод:

тип_результата имя_метода(аргументы);

// Так в интерфейсе объявляется свойство:

тип_свойства имя_свойства{

get; // Если свойству можно присвоить значение

set; // Если значение свойства можно прочитать

}

// Так в интерфейсе описывается индексатор:

тип this[индекс(ы)]{

get; // Если элементу можно присвоить значение

set; // Если можно прочитать значение элемента

}

}

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

собой  довольно  странную  конструкцию.  Вместе  с  тем  причины

к появлению интерфейсов в концептуальной парадигме языка C#

довольно просты и прозаичны и во многом связаны с тем, что в C#

нет множественного наследования (не путать с многоуровневым!).

Множественное наследование — это наследование, при котором

производный класс создается на основе сразу нескольких базовых

классов. Еще раз подчеркнем, что в C# (в отличие от C++) такая ситуа-

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

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

С другой стороны, множественное наследование — очень мощный

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

Компромисс  находят  в  том,  что  разрешают  реализовать  в  одном

классе сразу несколько интерфейсов. Таким образом, в одном классе

объединяются разные группы методов — как если бы при множе-

ственном наследовании. При этом описание методов выполняется

непосредственно в классе, что позволяет контролировать взаимную

корректность кода.

Хотя методы в интерфейсе только объявляются (то есть, по сути, являют-

ся абстрактными), ключевое слово abstract здесь не указывается. Более

того, по умолчанию все они считаются открытыми. Что касается свойств и

Интерфейсы           229

индексаторов, то, как отмечалось, у них не описываются аксессоры. Если

соответствующий член интерфейса доступен для присваивания значения, в его теле (в фигурных скобках) указывается ключевое слово get (намек на

аксессор для присваивания значения). Если свойство/индексатор доступ-

ны для считывания значения, указывается ключевое слово set.

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

цесс создания класса на основе интерфейса называется реализацией ин-

терфейса. Интерфейс, который реализуется в классе, указывается при

описании класса через двоеточие после имени класса — так же, как при

наследовании классов. Один класс может реализовывать сразу несколь-

ко интерфейсов. В этом случае интерфейсы указываются через запятую.

В классе, который реализует интерфейс (или интерфейсы), необходимо

описать те методы (и аксессоры), которые объявлены в интерфейсе (или

интерфейсах). Если, помимо реализации интерфейсов, класс создается

еще и на основе базового класса, то этот базовый класс возглавляет список

реализуемых интерфейсов.

Простой пример использования интерфейса приведен в программном

коде в листинге 6.4. Соответствующий проект реализуется как Windows-

приложение. Результат наших программных изысканий, реализуемых че-

рез приведенный ниже программный код, предстанет в виде окна с двумя

кнопками и текстовой меткой по центру окна. Щелчок на кнопке Отмена

приводит к закрытию окна и завершению работы приложения. Щелчок на

кнопке OK приводит к изменению тестового содержимого метки — в тексте

содержится информация о том, сколько раз выполнялся щелчок на кнопке

OK. Теперь приступим к анализу программного кода.

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

граммирования от этого не пострадало бы. Но мы программировать

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

эту ситуацию можно посмотреть и по-иному: интерфейсы настолько

хороши, что не помешают в любой ситуации.

Листинг 6.4.  Знакомство с интерфейсами

using System;

using System.Drawing;

using System.Windows.Forms;

// Описание интерфейса:

interface IBase{

// Интерфейсный индексатор:

продолжение

230

Глава 6. Важные конструкции

Листинг 6.4 (продолжение)

Button this[bool s]{

get; // Аксессор для считывания значения

set; // Аксессор для присваивания значения

}

// Интерфейсное свойство:

string text{

set; // Аксессор для присваивания значения

}

// Интерфейсный метод (для обработки

// щелчка на кнопке):

void OnBtnClick(Object btn,EventArgs ea);

// Интерфейсный метод (для изменения

// текста метки):

void textChange();

}

// Класс, наследующий класс Form и

// реализующий интерфейс IBase:

class MForm:Form,IBase{

// Закрытое поле - ссылка на объект кнопки:

private Button bOK;

// Еще одно закрытое поле - ссылка на кнопку:

private Button bCancel;

// Закрытое поле - ссылка на текстовую метку:

private Label lbl;

// Закрытое целочисленное поле-счетчик:

private int count;

// Индексатор:

public Button this[bool s]{

get{ // Аксессор для считывания значения

if(s) return bOK;

else return bCancel;

}

set{ // Аксессор для присваивания значения

if(s) bOK=value;

else bCancel=value;

}

}

// Свойство:

public string text{

set{ // Аксессор для присваивания значения

lbl.Text=value;

}

}

// Конструктор класса:

public MForm(){

Интерфейсы           231

// Положение и размер окна формы:

Bounds=new Rectangle(500,300,450,250);

// Тип границы формы:

FormBorderStyle=FormBorderStyle.Fixed3D;

// Заголовок окна формы:

Text="Окно с двумя кнопками";

int h=30; // Высота кнопок

int w=150; // Ширина кнопок

// Создание объекта для шрифта:

Font fnt=new Font("Arial",13,FontStyle.Bold);

// Применяем шрифт для формы:

Font=fnt;

// Начальное значение для счетчика:

count=0;

// Создание первой кнопки:

this[true]=new Button();

// Текст первой кнопки:

this[true].Text="OK";

// Положение и размеры кнопки:

this[true].Bounds=new Rectangle(50,180,w,h);

// Создание второй кнопки:

this[false]=new Button();

// Текст второй кнопки:

this[false].Text="Отмена";

// Положение и размер кнопки:

this[false].SetBounds(250,180,w,h);

// Создание делегата обработчика сразу

// для двух кнопок:

EventHandler eh=new EventHandler(OnBtnClick);

// Регистрация делегата для первой кнопки:

this[true].Click+=eh;

// Регистрация делегата для второй кнопки:

this[false].Click+=eh;

// Создание текстовой метки:

lbl=new Label();

// Положение и размеры области метки:

lbl.SetBounds(50,30,350,120);

// Способ выравнивания текста в области метки:

lbl.TextAlign=ContentAlignment.MiddleCenter;

// Присваивание (неявное) текстового

// значения метке:

textChange();

// Добавление текстовой метки в окно формы:

Controls.Add(lbl);

// Добавление первой кнопки в окно формы:

Controls.Add(this[true]);

продолжение

232

Глава 6. Важные конструкции

Листинг 6.4 (продолжение)

// Добавление второй кнопки в окно формы:

Controls.Add(this[false]);

}

// Метод для обработки щелчков на кнопках:

public void OnBtnClick(Object btn,EventArgs ea){

// Проверяем, на какой кнопке выполнен щелчок:

if(btn==this[true]){ // Если щелкнули на первой кнопке

count++;

textChange();

}

else Application.Exit(); // Если щелкнули на второй кнопке

}

// Метод для изменения текстового свойства:

public void textChange(){

// Значение текстового свойства - оно же

// текстовое значение метки:

text="Кнопка OK нажата "+count+" раз!";

}

}

// Класс с главным методом программы:

class InterfaceDEmo{

// Инструкция выполнять программу

// в едином потоке:

[STAThread]

// Главный метод программы:

public static void Main(){

// Отображение окна:

Application.Run(new MForm());

}

}

Поскольку с интерфейсом мы сталкиваемся впервые, имеет смысл оста-

новиться на его программном коде подробнее. Итак, в программе описан

интерфейс IBase. Для этого использован следующий программный код: interface IBase{

Button this[bool s]{

get;

set;

}

string text{

set;

}

void OnBtnClick(Object btn,EventArgs ea);

void textChange();

}

Интерфейсы           233

Заголовок интерфейса состоит из ключевого слова interface и име-

ни интерфейса IBase. В интерфейсе описаны два метода, свойство и ин-

дексатор. Описание начинается с индексатора. Заголовок индексатора

Button this[bool s] означает, что элементом индексатора является объ-

ектная ссылка типа Button (то есть объект кнопки). Индексом индексатора

может выступать переменная логического типа bool. Таким образом мы

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

индексатор мы впоследствии «спрячем» две кнопки нашей оконной фор-

мы. Аксессоры в индексаторе не описаны. Там только есть ключевые слова

get и set. Это говорит о том, что индексатор должен иметь как аксессор для

доступа к значению индексатора, так и аксессор для присваивания значе-

ния индексатору.

Свойство текстовое и называется text. Тело свойства содержит единствен-

ную инструкцию set. Поэтому при определении свойства в классе, кото-

рый реализует свойство, нужно будет описать только аксессор для присва-

ивания значения свойству. Забегая вперед заметим, что в свойство будет

«упаковано» текстовое содержимое метки формы.

Объявленный в интерфейсе метод void OnBtnClick(Object btn,EventArgs ea) имеет все признаки обработчика события — он не возвращает результат

и имеет «правильные» аргументы. Мы будем использовать этот метод, по-

сле определения его кода в классе, именно как обработчик. Причем здесь

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

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

кнопке. Поэтому метод, как мы увидим это далее, определяется так, что

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

щелчок.

Еще один объявленный в интерфейсе метод void textChange() также не

возвращает результат, и у него нет аргументов. Через этот метод мы реа-

лизуем процесс изменения текстового значения метки формы. Но все это

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

с заголовком class MForm:Form,IBase. Класс MForm создается путем насле-

дования библиотечного класса Form и реализует интерфейс IBase. Послед-

нее обстоятельство означает, что в классе MForm должны быть описаны все

методы, свойства и индикаторы, объявленные в интерфейсе IBase. Но кое-

что в классе есть и свое. Так, у класса есть два закрытых поля, bOK и bCancel, класса Button (кнопки), а также закрытое поле lbl класса Label (текстовая

метка). Еще имеется закрытое целочисленное поле count, которое призва-

но служить счетчиком количества щелчков на первой кнопке (первой в на-

шем случае будет кнопка bOK). Также в классе описывается то, что должно

быть описано, равно как и конструктор класса.

Описание индексатора — это, по большому счету, описание его аксессоров

(тех из них, что объявлены в интерфейсе). Заголовок индексатора такой

234

Глава 6. Важные конструкции

же, как в интерфейсе — за исключением, разве что, атрибута public, кото-

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

интерфейса, описываемых в классе, реализующем интерфейс.

ПРИМЕЧАНИЕ Хотя члены интерфейса описываются без атрибута уровня доступа, по умолчанию все они являются открытыми. Поэтому при описании

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

атрибут public.

У индексатора имеются оба аксессора. Аксессор для считывания значения

определяется с помощью условного оператора. В условном операторе про-

веряется индекс индексатора. Поскольку это логическое значение, такая

ситуация корректна. Если индекс равен true, в качестве результата возвра-

щается ссылка на кнопку bOK. В противном случае возвращается ссылка

на кнопку bCancel. По похожей схеме выполняется и set-аксессор. Если

индекс индексатора равен true, значение присваивается переменной bOK, а в противном случае значение присваивается переменной bCancel. Хотя

острой необходимости в этом нет, ссылки на кнопки мы будем выполнять

через индексатор.

Текстовое свойство text содержит описание set-аксессор, в котором ко-

мандой lbl.Text=value присваивается значение свойству Text метки lbl.

Поэтому, обращаясь к свойству text, мы на самом деле будем обращаться

к свойству lbl.Text. Как говорится, мелочь, а приятно.

Все самое интересное происходит в конструкторе класса. Некоторые ко-

манды конструктора нам уже знакомы. А некоторые знакомые операции

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

Например, положение и размер окна формы мы задаем «одним махом», с по-

мощью команды Bounds=new Rectangle(500,300,450,250). Здесь свойству

Bounds формы в качестве значения присваивается экземпляр структуры

Rectangle. Аргументами конструктору передаются четыре целочисленных

значения. Первые два определяют положение (координаты относительно

левого верхнего угла экрана) окна формы на экране, а два других — шири-

на и высота окна формы соответственно. Чтобы можно было использовать

структуру Rectangle, в шапку программного кода была добавлена инструк-

ция подключения пространства имен using System.Drawing.

Тип границы формы определяется командой FormBorderStyle=FormBorder­

Style.Fixed3D. Константа Fixed3D перечисления FormBorderStyle означает, что у формы будут объемные края, что придает форме эффект вдавлива-

ния. Заголовок окна формы определяется командой Text="Окно с дву­

мя кнопками". Целочисленные переменные h и w мы вводим для удобства.

Они определяют высоту и ширину кнопок.

Интерфейсы           235

Мы потихоньку начинаем проявлять наши дизайнерские наклонности.

Начнем с малого — переопределим шрифт для формы. Для начала нам

нужно создать объект, который впитал бы в себя наши представления об

удачном шрифте. Исполненные решимости, командой Font fnt=new Font ("Arial",13,FontStyle.Bold) создаем объект класса Font. Этот объект со-

ответствует жирному шрифту типа Arial размера 13. Чтобы использовать

этот шрифт в форме, ссылку на созданный объект следует присвоить свой-

ству Font формы, что мы, собственно, и делаем командой Font=fnt. На этом

блок команд по настройке параметров формы завершен.

Командой count=0 для надежности присваиваем начальное нулевое значе-

ние счетчику count.

«Для надежности» — потому что по умолчанию поле и так получит

нулевое  значение.  Но  в  жизни  действует  один  простой  принцип:

«Хочешь, чтобы все было сделано правильно — сделай сам». Поэтому

на случай не полагаемся и, несмотря ни на что, присваиваем полю

начальное нулевое значение.

Первую кнопку (объект) создаем командой this[true]=new Button().

В данном случае вместо прямой ссылки bOK мы использовали индексатор

this[true]. Соответственно, для второй кнопки вместо ссылки bCancel бу-

дем использовать индексатор this[false], а команда создания этой кнопки

имеет вид this[false]=new Button(). И здесь важно понимать, что никакой

необходимости в таком пижонстве нет.

Текст первой кнопки задаем командой this[true].Text="OK", а размеры

и по ложение — с помощью команды this[true].Bounds=new Rectangle(50, 180,w,h). Нечто похожее мы уже видели. Только раньше речь шла о свойстве

Bounds формы, а теперь это свойство кнопки. Поэтому экземпляр структу-

ры Rectangle определяет в данном случае положение в форме кнопки (два

первых аргумента конструктора) и ее геометрические параметры (два дру-

гих аргумента конструктора). Название второй кнопки определяем с помо-

щью команды this[false].Text="Отмена". Что касается положения кнопки

и границ, то для второй кнопки мы используем подход, альтернативный

рассмотренному выше. Командой this[false].SetBounds(250,180,w,h) за-

даем нужные настройки. Здесь мы прибегли к помощи метода SetBounds(), аргументы которого имеют тот же смысл, что и аргументы конструктора

структуры Rectangle.

Создание делегата обработчика сразу для двух кнопок выполняется коман-

дой EventHandler eh=new EventHandler(OnBtnClick). Таким образом, экзем-

пляр делегата eh ссылается на метод OnBtnClick(). Регистрируем экземпляр

236

Глава 6. Важные конструкции

делегата командами this[true].Click+=eh (регистрация для первой кноп-

ки) и this[false].Click+=eh (регистрация для второй кнопки).

Текстовая метка создается уже знакомым для нас способом — командой

lbl=new Label(). Положение и размеры области метки задаем с помощью

метода SetBounds(), вызвав его в команде lbl.SetBounds(50,30,350,120).

Способ выравнивания текста в области метки (по высоте — выравнивание

по середине, по горизонтали — выравнивание по центру) определяется ко-

мандой lbl.TextAlign=ContentAlignment.MiddleCenter. Чтобы присвоить

тексту метки значение, вызываем метод textChange().

Что касается метода textChange(), то определен он достаточно про-

сто: в теле метода командой text=«Кнопка OK нажата "+count+" раз!»

текстовому свойству text присваивается строка, содержащая, кроме

прочего, текущее значение счетчика count. Счетчик этот имеет на-

чальное  нулевое  значение,  как  мы  увидим  дальше,  увеличивается

на единицу каждый раз, когда пользователь выполняет щелчок на

первой кнопке (кнопка OK).

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

зуем метод Controls.Add(): текстовую метку добавляем командой Controls.

Add(lbl), кнопки добавляются командами Controls.Add(this[true]) и Controls.Add(this[false]).

Как отмечалось ранее, поскольку обе кнопки регистрируют обработчиком

щелчка один и тот же метод (а именно, метод OnBtnClik()), то метод для об-

работки щелчков на кнопках должен иметь возможность как-то эти кнопки

«различать». И здесь на помощь приходит первый аргумент метода. Имен-

но этот аргумент «знает», на какой кнопке выполнен щелчок. Более того, объект является ссылкой на объект, вызвавший событие. Поэтому резуль-

татом выражения btn==this[true], которое указано условием в условном

операторе в теле метода, является значение true, если щелчок выполнен

на первой кнопке, и false в противном случае (методом исключения полу-

чается, что в этом случае щелчок выполнен на второй кнопке). Для первой

кнопки (если щелчок выполнен на ней) предназначены команды count++

(увеличение значения счетчика щелчков на первой кнопки) и textChange() (изменение текстового значения метки). Для случая, когда щелчок вы-

полнен на второй кнопке, команда одна — инструкция завершить работу

Application.Exit().

В главном методе программы командой Application.Run(new MForm()) за-

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

ется графическое окно с двумя кнопками и текстом. Текст сообщает о том, что кнопка OK еще ни разу не нажималась. Как выглядит окно в самом на-

чале выполнения программы, показано на рис. 6.5.

Интерфейсные переменные           237

Рис. 6.5.  Так выглядит окно при запуске программы

Как ситуация будет разворачиваться в дальнейшем, зависит во многом от

наших героических действий. Каждый наш щелчок на кнопке OK приводит

к тому, что на единицу увеличивается число щелчков на кнопке в тексто-

вом сообщении в центральной части окна. На рис. 6.6 показано, как будет

выглядеть окно после нескольких щелчков на кнопке OK.

Рис. 6.6.  Вид окна после нескольких щелчков на кнопке OK —

изменилось содержание текстового поля

Но стоит нам щелкнуть на кнопке Отмена, как окно будет закрыто, а работа

приложения завершена.

Интерфейсные переменные

Эта теория недостаточно безумна, чтобы быть верной.

Н. Бор

Есть одна очень интересная и полезная особенность интерфейсов. Заклю-

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

238

Глава 6. Важные конструкции

интерфейса. Такие переменные называются интерфейсными. Во многом

интерфейсные переменные напоминают объектные переменные. Как

и объектная переменная, интерфейсная переменная может ссылаться на

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

того же класса. И здесь все выглядит вполне логично. А на какой объект

может ссылаться интерфейсная переменная? Ведь для интерфейса объ-

ект не создается. Ответ простой и несколько неожиданный: интерфейсная

переменная может ссылаться на объект любого класса, который реализует

интерфейс. Правда, имеется одно существенное ограничение: через интер-

фейсную ссылку (переменную) доступ есть только к тем членам класса, которые описаны в реализуемом интерфейсе. Это непримечательное на

первый взгляд обстоятельство имеет далеко идущие последствия. Чтобы

не быть голословными, сразу обратимся к примеру, который представлен

в листинге 6.5.

Листинг 6.5.  Интерфейсные переменные

using System;

// Интерфейс с одним объявленным методом:

interface IMath{

// Метод с целочисленным аргументом

// и целочисленным результатом:

int GetNumber(int n);

}

// Класс, реализующий интерфейс:

class Factorial:IMath{

// Метод для вычисления факториала числа:

public int GetNumber(int n){

int res=1; // Начальное значение

// переменой-результата

for(int i=2;i<=n;i++){ // Вычисление факториала

res*=i;

}

return res; // Результат

}

}

// Еще один класс, реализующий интерфейс:

class Fibonacci:IMath{

// Метод для вычисления чисел Фибоначчи:

public int GetNumber(int n){

int a=1,b=1; // Начальные числа последовательности

for(int i=3;i<=n;i++){ // Вычисление чисел

// последовательности

b=a+b; // Последнее число

a=b-a; // Предпоследнее число

Интерфейсные переменные           239

}

return b; // Результат

}

}

// Класс с главным методом программы:

class IRefDemo{

// Главный метод программы:

public static void Main(){

// Интерфейсная переменная:

IMath r;

// Ссылка на объект класса Factorial:

r=new Factorial();

// Вызов метода GetNumber() через

// интерфейсную ссылку (переменную):

Console.WriteLine("Факториал числа 10!={0}.",r.GetNumber(10));

// Ссылка на объект класса Fibonacci():

r=new Fibonacci();

// Вызов метода GetNumber() через

// интерфейсную ссылку (переменную):

Console.WriteLine("10-е число Фибоначчи:

{0}.",r.GetNumber(10));

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

В программе есть интерфейс IMath, у которого объявлен единственный

метод GetNumber(). У метода — один целочисленный аргумент, и метод

возвращает целочисленный результат. Еще в программе есть два клас-

са: Factorial и Fibonacci. Каждый из этих классов реализует интерфейс

IMath. В каждом из этих классов описывается метод GetNumber(), но опи-

сывается по-разному. В классе Factorial этот метод вычисляет факто-

риал числа, а в классе Fibonacci метод описан так, что вычисляет число

Фибоначчи.

В главном методе программы командой IMath r объявляется интерфейсная

переменная r. В качестве типа такой переменной указывается имя интер-

фейса IMath. Командой r=new Factorial() в качестве значения этой интер-

фейсной переменной присваивается ссылка на объект класса Factorial.

Это можно делать, поскольку класс Factorial реализует интерфейс IMath.

При этом, вызывая метод GetNumber() через переменную r, вызываем на

самом деле метод, определенный в классе Factorial. Эта ситуация «про-

веряется» в команде Console.WriteLine("Факториал числа 10!={0}.",r.

GetNumber(10)). После этого командой r=new Fibonacci() переменной r присваивается ссылка на объект класса Fibonacci. Этот класс тоже реа-

лизует интерфейс IMath. Теперь при вызове метода GetNumber() через

240

Глава 6. Важные конструкции

интерфейсную переменную r выполняется код из класса Fibonacci. Так

и происходит при выполнении команды Console.WriteLine("10-е чис­

ло Фибоначчи: {0}.",r.GetNumber(10)). Результат выполнения програм-

мы показан на рис. 6.7.

На всякий случай приведем краткие пояснения по поводу вычисления

результата при определении метода GetNumber() в классах Factorial и Fibonacci. Начнем с класса Factorial — там все проще. Локальная

переменная res получает начальное значение 1, после чего в опе-

раторе цикла последовательно умножается на значение индексной

переменной, которая пробегает значения от 2 до n (аргумент метода).

В результате получаем произведение натуральных чисел до n вклю-

чительно. Это и есть результат метода.

При вычислении числа Фибоначчи в классе Fibonacci переменным

a  и  b  присваиваются  единичные  значения.  Идея  в  том,  что  пере-

менная  a  «помнит»  предпоследнее  число  в  последовательности, а переменная b «помнит» последнее число в последовательности.

В операторе цикла за один цикл вычисляется следующая пара зна-

чений. Для этого выполняются команды b=a+b и a=b-a. В результате

переменная b получает новое значение (это сумма двух предыдущих

значений), а значение переменной a равно тому значению, которое

имела переменная b. Действительно, предположим, что в какой-то

момент значение переменной a равно , а значение переменной b равно  .  Нам  нужно  добиться  того,  чтобы  значение  переменой  b стало +, а значение переменной a стало равным . После вы-

полнения команды b=a+b переменная b имеет новое значение +, а у переменной a осталось старое значение . Как из значений +

(переменная b) и  (переменная a) получить значение ? Очень

просто — от одного значения отнять другое, для чего и использована

команда a=b-a.

Рис. 6.7.  Результат выполнения программы с интерфейсной переменной

Таким образом, мы дважды использовали инструкцию r.GetNumber() и по-

лучали разные результаты, в зависимости от того, на какой объект ссыла-

лась интерфейсная переменная r на момент вызова метода GetNumber().

Интерфейсные переменные           241

Ситуация с интерфейсными ссылками/переменными может быть доста-

точно нетривиальной, особенно если речь идет о реализации в классе сра-

зу нескольких интерфейсов. Но обсуждение всех возможных вариантов

в наши планы не входит. Более того, напомним, что способностью ссылать-

ся на «чужие» объекты обладают не только интерфейсные переменные, но

и объектные переменные базовых классов. Такие объектные переменные

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

Методы и классы

во всей красе

Я предупреждал. У джентльменов нет

оснований обижаться на меня.

Из к/ф «В поисках капитана Гранта»

Нами достигнуты некоторые успехи. Мы уже можем создавать приложение

с окном и кнопкой, умеем перегружать операторы, знакомы с наследовани-

ем и не пугаемся при слове «интерфейс». Может создаться впечатление, что ничего интересного в C# уже не осталось. Конечно, это совсем не так.

Часть наших иллюзий развеется в этой главе. Ее мы посвятим рассмотре-

нию тех вопросов и особенностей языка, которые мы оставили «за кавыч-

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

венно говоря, материал главы несколько эклектичен. Вместе с тем вопросы

здесь мы рассмотрим полезные, а где-то, может, даже и интересные.

Механизм передачи аргументов методам

Что касается смелости, тут я спорить не

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

Из к/ф «Семнадцать мгновений весны»

До этого мы смело использовали методы, в том числе и с аргументами.

И никаких особых проблем по поводу того, как передавать аргументы

Механизм передачи аргументов методам           243

методам, у нас не возникало даже на горизонте. То есть как бы даже на-

мека на возможные проблемы у нас не было. Но реальность обманчива

и иллюзорна. Чтобы не быть голословными, просто рассмотрим пример.

Обратимся к листингу 7.1. Сразу отметим, что, хотя формально код (син-

таксис) в листинге правильный, выполняется он не так, как можно было

бы ожидать.

Листинг 7.1.  Передача аргументов по значению

using System;

class SmallTrouble{

// Статический метод для обмена значениями

// аргументов

// (выполняется, но долг свой не выполняет):

static void swap(int a,int b){

// Значения аргументов до обмена значениями:

Console.WriteLine("До обмена: a={0} и b={1}.",a,b);

// Обмен значениями:

int t=b;

b=a;

a=t;

// Значения аргументов после обмена значениями:

Console.WriteLine("После обмена: a={0} и b={1}.",a,b);

}

public static void Main(){

// Целочисленные переменные:

int a=10,b=200;

// Производим "обмен":

swap(a,b);

// Проверяем результат:

Console.WriteLine("Проверяем: a={0} и b={1}.",a,b);

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

Мы начнем с классической ситуации. В классе SmallTrouble кроме метода

Main() есть еще один статический метод — swap(). Метод не возвращает

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

создать блиц-портрет для этого метода, его характеристика звучала бы так: метод «обменивает» значения аргументов — при вызове метода перемен-

ные, указанные аргументами, обмениваются значениями.

На самом деле ничем эти переменные не обмениваются — и в этом

нам предстоит убедиться.

244

Глава 7. Методы и классы во всей красе

Хотя код метода тривиальный, выполняется он «неожиданно», поэто-

му проанализируем метод swap() в деталях. Так, командой Console.

WriteLine("До обмена: a={0} и b={1}.",a,b) перед началом манипуляций

по обмену в консольное окно выводится окно с сообщением о том, каковы

значения аргументов, переданных методу (переменные a и b). Затем с по-

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

очень важный момент. Наконец, командой Console.WriteLine("После об­

мена: a={0} и b={1}.",a,b) проверяем результат наших наивных кальку-

ляций. Схема простая:

1. Проверили значения аргументов.

2. Поменяли значения аргументов.

3. Проверили значения аргументов.

В главном методе программы проверяем работу метода swap(). Для этого

создаем две целочисленные переменные a=10 и b=200 и передаем их аргу-

ментами методу swap(). После вызова метода с указанными аргументами

командой Console.WriteLine("Проверяем: a={0} и b={1}.",a,b) проверяем

значения переменных. Можно ожидать, что переменные должны обменять-

ся значениями. Но в глубине души мы понимаем, что, если бы это было

действительно так, не было бы смысла рассматривать этот пример. Наши

самые смелые прогнозы подтверждает результат выполнения программы, представленный на рис. 7.1.

Рис. 7.1.  Результат выполнения программы с «неправильным» методом

для обмена значениями аргументов

Что мы видим? При проверке значений аргументов в методе swap() все

выглядит очень прилично — обмен значениями у аргументов произошел, о чем свидетельствуют первые два сообщения в консольном окне. Но тре-

тье, последнее сообщение обескураживает — у переменных a и b значения

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

swap(), то переменные a и b демонстрируют полную лояльность. Но как

только метод завершил работу, все становится как раньше. Причины этих

симуляций со стороны аргументов метода swap() объясняются очень про-

сто (просто, но странно) — при передаче аргументов методу на самом деле

Механизм передачи аргументов методам           245

передаются не те переменные, что указаны аргументами, а их копии. При-

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

такой банальной подмены не замечали, поскольку не пытались в методах

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

ПРИМЕЧАНИЕ Здесь имеются в виду аргументы необъектных типов — те аргу-

менты, которые не относятся к объектным переменным. При пере-

даче  объектной  переменной  аргументом  для  нее  тоже  создается

копия. Но поскольку копия ссылается на тот же самый объект, что

и оригинал, то для объектных переменных ситуация с клонами не

столь трагична.

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

значению и состоит в том, что на самом деле в метод передается копия ар-

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

стоит в том, что в метод передается непосредственно та переменная, что

указана аргументом. По умолчанию аргументы передаются по значению.

Если не предпринимать никаких дополнительных усилий, то вместо тех

переменных, что указаны аргументами методов, в методы будут переда-

ваться копии этих переменных. Особенность этих копий в том, что они су-

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

свою работу, все локальные переменные, в том числе и копии переменных-

аргументов, автоматически уничтожаются.

Теперь нам легко объяснить специфическую работу метода swap(). Ког-

да выполняется команда swap(a,b), для переменных a и b автоматически

создаются копии и все операции в методе выполняются с этими копия-

ми. Именно копии обмениваются значениями. Поэтому, когда проверя-

ется результат обмена, все выглядит пристойно — поскольку обмен дей-

ствительно состоялся. Но обмен на уровне копий! Оригиналы остались

неизменными, в чем мы и убеждаемся, когда проверяем значения пере-

менных a и b после вызова метода swap(). Вот такая получается «война

клонов».

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

и без нашего вмешательства, возникает вопрос: как и где нам нужно «вме-

шаться» в программный код, чтобы аргументы передавались по ссылке?

Как и все в C#, здесь ответ простой: при описании метода и его вызове

аргументы, которые передаются по ссылке, необходимо указать с атрибу-

том ref. В листинге 7.2 приведен пример программы с «исправленным»

методом swap().

246

Глава 7. Методы и классы во всей красе

Листинг 7.2.  Передача аргументов по ссылке

using System;

class NoTrouble{

// Статический метод для обмена значениями

// аргументов

// (выполняется так, как надо):

static void swap(ref int a,ref int b){

// Значения аргументов до обмена значениями:

Console.WriteLine("До обмена: a={0} и b={1}.",a,b);

// Обмен значениями:

int t=b;

b=a;

a=t;

// Значения аргументов после обмена значениями:

Console.WriteLine("После обмена: a={0} и b={1}.",a,b);

}

public static void Main(){

// Целочисленные переменные:

int a=10,b=200;

// Производим правильный "обмен":

swap(ref a,ref b);

// Проверяем результат:

Console.WriteLine("Проверяем: a={0} и b={1}.",a,b);

// Ожидание нажатия клавиши Enter:

Console.ReadLine();

}

}

Результат выполнения этой программы представлен на рис. 7.2.

Рис. 7.2.  Результат выполнения программы с «правильным» методом

для обмена значениями аргументов

Несложно убедиться, что переменные a и b в результате выполнения ко-

манды swap(ref a,ref b) действительно обменялись значениями.

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

типа. В качестве небольшой иллюстрации рассмотрим пример в лис-

тинге 7.3.

Механизм передачи аргументов методам           247

По сравнению с первоначальным примером, изменения в программ-

ный код внесены лишь в двух местах. Во-первых, заголовок метода

описан как static void swap (ref int a,ref int b), и, во-вторых, при вы-

зове метода использована инструкция swap(ref a,ref b).

Листинг 7.3.  Изменение аргументов ссылочных типов

using System;

// Класс с целочисленным полем:

class Nums{

// Открытое целочисленное поле:

public int num;

// Конструктор с одним аргументом:

public Nums(int n){

num=n;

}

// Метод для отображения значения

// целочисленного поля:

public void show(){

Console.WriteLine("поле объекта: "+num);

}

}

// Класс с несколькими статическими методами:

class RefDemo{

// Статический метод для увеличения на единицу

// значения поля объекта-аргумента:

public static void up(Nums obj){

// Увеличиваем на единицу значение поля

// объекта-аргумента:

obj.num++;

// Текстовое сообщение в консольное окно:

Console.Write("Объект-аргумент: ");

obj.show(); // Отображение значения поля объекта

}

// Статический метод для "обмена"

// объектными ссылками.

// Аргументы передаются по ссылке:

public static void swap(ref Nums x,ref Nums y){

Nums t=x; // Локальная объектная переменная

x=y;

y=t;

// Проверка результата:

Console.Write("Первый объект-аргумент: ");

продолжение

248

Глава 7. Методы и классы во всей красе

Листинг 7.3 (продолжение)

x.show(); // Отображение поля первого

// объекта-аргумента

Console.Write("Второй объект-аргумент: ");

y.show(); // Отображение поля второго

// объекта-аргумента

}

// Главный метод программы:

public static void Main(){

// Создаем объекты класса Nums:

Nums a=new Nums(10);

Nums b=new Nums(200);

// Изменяем объект - увеличиваем значение поля:

up(a);

Console.Write ("Проверка: ");

a.show(); // Проверяем результат увеличения поля

// Объектные переменные обмениваются

// значениями:

swap(ref a,ref b);

// Проверка результата обмена:

Console.Write ("Проверка. Первый объект: ");

a.show(); // Отображение значения поля

// первого объекта

Console.Write ("Проверка. Второй объект: ");

b.show();// Отображение значения поля

// второго объекта

// Ожидание нажатия какой-нибудь клавиши:

Console.ReadKey();

}

}

Мы описываем класс Nums, у которого есть целочисленное поле num, кон-

структор с одним аргументом, а также метод show(), который позволяет

отобразить в консольном окне значение поля num. Объекты класса Nums будут «подопытными кроликами», на которых мы проверим корректность

выполнения двух статических методов. Методы называются up() и swap(), и описаны они в классе RefDemo (в этом классе, кстати, описан и главный ме-

тод программы). Оба метода не возвращают результат. У метода up() один

аргумент — это объект obj класса Nums. Аргумент передается «в обычном

режиме» — инструкция ref не используется. В ней просто нет необходимо-

сти. В теле метода командой obj.num++ на 1 увеличивается значение поля

num объекта-аргумента obj, а результат изменений проверяется командой

obj.show(). Благодаря этому мы узнаем, как ситуация с увеличением поля

объекта-аргумента выглядит изнутри метода up().

Механизм передачи аргументов методам           249

У метода swap() два аргумента (обозначены как a и b), и оба являются

объектами класса Nums. Метод с таким названием традиционно использу-

ется нами для взаимовыгодных обменов. В данном случае обмениваться

значениями будут объектные переменные. То, что до вызова метода было

первым объектом, станет вторым, а второй объект станет первым. Причем

аргументы, несмотря на то что они относятся к ссылочному типу (это объ-

ектные переменные), передаются по ссылке — оба аргумента метода опи-

саны с инструкцией ref. В теле метода объектные переменные по традици-

онной схеме меняются местами: переменная a будет ссылаться на объект, на который первоначально ссылалась переменная b, а переменная b, в свою

очередь, будет ссылаться на тот объект, на который до вызова метода ссы-

лалась переменная a. С помощью команд a.show() и b.show() мы проверя-

ем, каковы значения полей объектов a и b после обмена значениями аргу-

ментов метода.

В главном методе программы мы создаем два объекта класса Nums: объект

a со значением поля 10 и объект b со значением поля 200. После выполне-

ния команды up(a) значение поля a увеличивается с 10 до 11. Результат

проверяем командой a.show(). Затем командой swap(ref a,ref b) меняем

объекты a и b местами. Поверка последствий осуществляется с помощью

команд a.show() и b.show(). Результат выполнения программы представ-

лен на рис. 7.3.

Рис. 7.3.  Изменение аргументов ссылочных типов:

результат выполнения программы

Вывод у нас один — все работает правильно. Об этом свидетельствует

хотя бы тот факт, что после выполнения соответствующих манипуляций

в статических методах проверка внутри метода и проверка по завершении

метода дает одинаковые и вполне объяснимые результаты. Но у нас все

же закрадывается подспудное сомнение по поводу метода swap(): а может, все работало бы и без использования передачи аргументов этому методу

по ссылке? Другими словами, вопрос такой: будет ли все выполняться так

же корректно, если из программного кода удалить все инструкции ref?

Ответ такой: нет, не будет. Желающие могут проделать процедуру само-

стоятельно: в программном коде листинга 7.3 удалить четыре инструкции

250

Глава 7. Методы и классы во всей красе

ref — две в описании метода swap() и две в команде вызова этого метода.

Если после этого запустить команду на выполнение, получим результат, как на рис. 7.4.

Рис. 7.4.  Изменение аргументов ссылочных типов:

некорректный обмен ссылок в объектных переменных

Обратите внимание: по сравнению с предыдущим случаем в консоль-

ном окне изменились две последние строки.

На словах добавим, что метод up() свою работу выполняет честно, хотя

ему аргумент как передавался по значению, так и передается. А вот метод

swap() местами объекты не поменял, хотя при проверке внутри метода все

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

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

рассуждений обозначим аргументы, которые передаются методу, как a и b.

Хотя это и объектные переменные, при их передаче в качестве аргументов

автоматически создаются копии — назовем их A и B. Значение копии A та-

кое же, как и переменной a, а значение копии B такое же, как и переменной

b. Поэтому переменные a и A ссылаются на один и тот же объект, и пере-

менные b и B ссылаются на один и тот же объект. Но вот операции по об-

мену выполняются с копиями. Поэтому после того, как обмен произведен, копия A ссылается на объект b, а копия B ссылается на объект A, что и под-

тверждает вызов метода show() в теле статического метода swap(). Здесь

следует помнить, что метод show() вызывается из объектов-копий. А что

же с переменными a и b? Их значения стались прежними, в чем мы и убеж-

даемся после завершения работы метода swap(). Вот почему методу swap() аргументы нужно передавать по ссылке несмотря на то, что они являются

переменными ссылочного типа.

С методом up() таких неприятностей не происходит. Объяснение тоже

достаточно простое. Если объектная переменная передается аргументом

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

Аргументы без значений и переменное количество аргументов           251

с объектом, а не с объектной переменной, результат такой, как надо, и без

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

Аргументы без значений и переменное

количество аргументов

Может, где-нибудь высоко в горах,

но не в нашем районе, вы что-нибудь

обнаружите для вашей науки.

Из к/ф «Кавказская пленница»

Есть два полезных механизма, связанные со способом определения аргу-

ментов метода, которые позволяют сделать программный код достаточно

гибким и эффективным, а иногда и просто эффектным. В этом разделе мы

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

фиксировано (то есть количество аргументов на момент описания мето-

да неизвестно), а также передачу аргументов методам без значений. В по-

следнем случае речь идет о том, что в C# разрешается (при определенном

стечении обстоятельств) передавать в качестве аргументов методам пере-

менные, которые объявлены, но которым не присвоено значение. Другое

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

Общий рецепт состоит в том, что для описания метода с нефиксированным

количеством аргументов использовать массив с идентификатором params.

Иначе говоря, если мы хотим описать метод, количество аргументов ко-

торого наперед неизвестно, аргумент метода описывается с атрибутом

params, а сам список аргументов отождествляется с массивом элементов

соответствующего типа. В качестве простенькой иллюстрации рассмотрим

пример, представленный в листинге 7.4. В этой программе описан метод, который позволяет вычислять среднее арифметическое значение для на-

бора числовых значений, переданных аргументами методу.

Листинг 7.4.  Метод с нефиксированным количеством аргументов

using System;

// Класс со статическим методом с переменным

// количеством аргументов:

class ParamsDemo{

// Метод с переменным количеством аргументов:

static double average(params double[] nums){

продолжение

252

Глава 7. Методы и классы во всей красе

Листинг 7.4 (продолжение)

double res=0; // Начальное значение

// переменной-результата

// Информационное сообщение:

Console.WriteLine("Числовой ряд:");

// Перебор элементов массива - аргументов

// метода:

foreach(double s in nums){

Console.Write(s+" "); // Аргумент отображается

// в консоли

res+=s; // Вычисляется сумма аргументов

}

Console.WriteLine(); // Переход к новой строке

// Вычисление среднего значения:

res/=nums.Length;

// Результат метода:

return res;

}

// Главный метод программы:

public static void Main(){

// Вызов метода с 10 аргументами:

double r=average(1,3,6,8,2,-4,2,1,-5,-3);

// Проверяем результат:

Console.WriteLine("Среднее значение равно "+r);

// Вызов метода с 15 аргументами:

r=average(-1,2,-5,8,2,-4,7,2,-1,5,10,-5,12,-7,-4,2);

// Проверяем результат:

Console.WriteLine("Среднее значение равно "+r);

// Ожидание нажатия какой-нибудь клавиши:

Console.ReadKey();

}

}

Сигнатура статического метода average(), который в качестве значения воз-

вращает число типа double, выглядит как average(params double[] nums).

Конечно, примечателен здесь способ описания аргумента (или аргумен-

тов — зависит от того, как на это все смотреть). Атрибут params подает нам

сигнал о том, что речь идет о методе, у которого может быть сколько угодно

числовых аргументов типа double. Формально эти аргументы интерпрети-

руются как массив, который мы назвали nums, а тип этого массива, в силу

очевидных причин, есть тип переменной массива с double-элементами. Та-

ким образом, при обработке аргументов метода average() иллюзия такая, как если бы аргументы были не отдельными числами, а числовым масси-

вом. Это удобно хотя бы потому, что при таком подходе количество аргу-

ментов в методе определяется как nums.Length.

Аргументы без значений и переменное количество аргументов           253

В теле метода инициализируется с нулевым начальным значением double-

переменная res. Эта переменная, после выполнения всех нужных вычисле-

ний, будет возвращаться как результат метода. А результатом метода, на-

помним, является среднее значение аргументов, которое определяется как

сумма аргументов, деленная на их количество. В операторе цикла foreach() перебираются все элементы массива. Элементы выводятся в консольном

окне в одну строку. Но не это главное. Главное то, что в результате выпол-

нения оператора цикла вычисляется сумма элементов массива — то есть

сумма аргументов метода. Сумма записывается в переменную res. После

завершения оператора цикла командой res/=nums.Length вычисляется

среднее значение. Оно и возвращается как результат.

В главном методе программы метод average() вызывается дважды с раз-

ным количеством аргументов. Результаты вычислений представлены на

рис. 7.5.

Рис. 7.5.  Метод с переменным количеством аргументов: результат выполнения программы

Обращаем внимание читателя на то, что, хотя при описании метода average() мы отталкивались от того, что его аргументы реализованы в виде массива, при вызове метода аргументы передаются простым перечислением в кру-

глых скобках после имени метода. Никаких массивов создавать не нужно.

Теперь обсудим способ передачи методу в качестве аргумента переменной, которой не присвоено значение. Сразу отметим, что вообще такая ситуа-

ция интерпретируется как ошибочная, поэтому, если уж мы используем

подобный экзотический код, нам предстоит каким-то образом предупре-

дить о наших планах компилятор. Благо предупредить его несложно. При

описании метода соответствующий аргумент объявляется с атрибутом out.

Такой же атрибут для аргумента указывается при вызове метода. Что ка-

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

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

разному, но один из возможных способов состоит в том, чтобы один из «ре-

зультатов» записывать в переменную, переданную аргументом методу. По-

нятно, что это далеко не единственный подход, но он допустим. Например,

254

Глава 7. Методы и классы во всей красе

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

ляет наибольшее и наименьшее значение. Вариантов организации такого

метода — неисчислимое множество. Один из них такой: наибольшее число

метод возвращает в качестве результата, а наименьшее число записывается

в переменную, которая передана первым аргументом методу. Именно та-

кой пример представлен в программном коде в листинге 7.5.

Листинг 7.5.  Аргумент метода — неинициализированная переменная

using System;

class OutDemo{

// Статический метод с неинициализированным

// первым аргументом:

static int MinMax(out int min,params int[] n){

// Начальное значение для результата метода:

int max=n[0];

// Минимальное значение:

min=n[0];

Console.WriteLine("Числовой ряд:"); // Сообщение в консоль

// Оператор цикла для перебора аргументов

// метода:

foreach(int s in n){

// Значение аргумента выводится в консоль:

Console.Write(s+" ");

// Группа условных операторов:

if(s>max) max=s; // Изменяем максимальное значение

if(s

}

// Переход к новой строке:

Console.WriteLine();

// Результат метода:

return max;

}

// Главный метод программы:

public static void Main(){

// Объявление целочисленных переменных:

int min,max;

// Вызов метода с первым неинициализированным

// аргументом:

max=MinMax(out min,1,0,-5,8,21,-9,11,-10,25,16);

// Сообщаем результат вычислений:

Console.WriteLine("Экстремальные значения: min={0}

и max={1}",min,max);

// Ожидание ввода символа:

Console.ReadKey();

}

}

Аргументы без значений и переменное количество аргументов           255

Статический метод с заголовком int MinMax(out int min,params int[] n) предназначен для вычисления минимального и максимального значений

среди набора числовых переменных, переданных аргументами методу.

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

аргументом методу. Этот аргумент метода описан как out int min, то есть

с атрибутом out. Количество прочих аргументов метода не фиксирова-

но, поэтому их мы описываем params int[] n, то есть с ключевым словом

params, как в предыдущем примере.

В теле метода переменной max, которую планируем возвращать в качестве

результата метода, записываем значение n[0] (второй аргумент в списке

аргументов метода — первым является переменная min для записи ми-

нимального значения). Такое же значение присваивается переменной-

аргументу min. Затем в операторе цикла перебираются аргументы метода.

Каждый элемент (значение) выводится на экран. Кроме того, каждый счи-

танный аргумент сравнивается с текущим минимальным и максимальным

значениями, и, если нужно, эти значения обновляются. Реализуется соот-

ветствующая проверка с помощью двух условных операторов.

В главном методе программы командой объявляются (но не инициализи-

руются) две целочисленные переменные, min и max, после чего с помощью

команды max=MinMax(out min,1,0,-5,8,21,-9,11,-10,25,16) эти перемен-

ные получают свои значения. По-разному, но получают: переменная max как результат метода MinMax(), а переменная min — как его аргумент. Ре-

зультат выполнения программы представлен на рис. 7.6.

Рис. 7.6.  Метод с неинициализированным аргументом: результат выполнения программы

Стоит заметить, что атрибут out (так же, как и атрибут ref, который

рассматривался ранее) указывается как при описании метода, так

и в команде вызова метода.

Кроме того, out-аргумент автоматически передается по ссылке, то есть

в метод передается «оригинал», а не «копия». Это вполне объяснимо, поскольку копию такого аргумента передавать в метод совершенно

нет никакого смысла.

256

Глава 7. Методы и классы во всей красе

Передача типа в качестве параметра

Зато так поступают одни лишь мудрецы,

Зато так наступают одни лишь храбрецы.

Из к/ф «Айболит 66»

Выше мы несколько раз описывали метод, который менял местами зна-

чения аргументов. Делали мы это для аргументов разных типов, но на

самом деле делали каждый раз одно и то же — в том смысле, что алгоритм

вычислений совершенно не зависел от типа аргументов. И такие ситуа-

ции встречаются достаточно часто. На какую мысль это нас наводит? На-

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

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

различных типов. Причем перегрузка метода в данном случае не очень

подходит, поскольку при перегрузке метода каждая его версия описыва-

ется явно. Здесь речь идет о программных кодах иного рода. Нас интере-

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

тип данных в виде параметра практически так же, как мы указываем ар-

гументы у метода.

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

сы, в которых тип данных является формальным параметром.

ПРИМЕЧАНИЕ Класс с параметрами типа называется обобщенным классом, а метод

с параметрами типа называется обобщенным методом.

Соответствующая процедура может быть применена как к отдельным

методам, так и к целым классам. Мы начнем с малого — с описания ме-

тодов. Здесь есть два момента, которые нужно иметь в виду, если мы хо-

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

роль. Во-первых, для типа данных следует ввести идентификатор, или

параметр типа. Другими словами, необходимо придумать обозначение

для типа данных. Это обозначение (которое и будем называть параме-

тром типа) указывается в угловых скобках сразу после имени метода.

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

формального типа, используем параметр типа из треугольных скобок.

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

глядности рассмотрим пример, в котором метод для обмена значениями

Передача типа в качестве параметра           257

своих аргументов реализован с использованием параметра типа. Пример

представлен в листинге 7.6.

Листинг 7.6.  Метод с параметром типа

using System;

// Класс пользователя:

class MyClass{

// Символьное поле:

public char s;

// Конструктор класса:

public MyClass(char s){

this.s=s;

}

}

// Класс содержит метод с параметром типа

// и главный метод программы:

class TypeParametersDemo{

// Метод с параметром типа.

// Идентификатор X обозначает тип данных:

static void swap(ref X a,ref X b){ // Два аргумента типа X

X t=a; // Локальная переменная типа X

// Присваивание переменных типа X:

a=b;

b=t;

}

// Главный метод программы:

public static void Main(){

// Объявляем и инициализируем

// целочисленные переменные:

int a=10,b=200;

// Объявляем и инициализируем

// текстовые переменные:

string A="Первый",B="Второй";

// Объявляем объектные переменные

// и создаем объекты:

MyClass objA=new MyClass('A');

MyClass objB=new MyClass('B');

// Вызываем метод с параметром типа:

swap(ref a,ref b); // Вместо X используем int

// Проверяем результат:

Console.WriteLine("Проверка: a={0} и b={1}.",a,b);

// Вызываем метод с параметром типа:

swap(ref A,ref B); // Вместо X используем string

// Проверяем результат:

Console.WriteLine("Проверка: A={0} и B={1}.",A,B); продолжение

258

Глава 7. Методы и классы во всей красе

Листинг 7.6 (продолжение)

// Вызываем метод с параметром типа:

swap(ref objA,ref objB); // Вместо X используем MyClass

// Проверяем результат:

Console.WriteLine("Проверка: objA->{0} и

objB->{1}.",objA.s,objB.s);

// Ожидание нажатия клавиши:

Console.ReadKey();

}

}

В программе для технических нужд описывается класс MyClass, у которого

есть одно символьное поле и конструктор с одним аргументом. В классе

TypeParametersDemo описывается статический метод swap(). Метод не воз-

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

скобках после имени метода. Сигнатура метода swap(ref X a,ref X b) означает буквально следующее:

 Идентификатор X обозначает какой-то определенный тип данных. Если

несколько переменных объявлены с типом X, то это означает, что все они

относятся к одному и тому же типу. Какой именно это тип — определя-

ется при вызове метода.

 Аргументы метода (их два) имеют тип X.

 Инструкция ref, как и ранее, означает, что аргументы типа X передаются

по ссылке.

В теле метода также встречается параметр типа X. Например, команду X t=a следует понимать так: объявляется локальная переменная типа X, и ей в ка-

честве значения присваивается переменная a. Эта команда корректна, по-

скольку обе переменные относятся к одному и тому же типу X. Правда, мы

пока не знаем, что это за тип, но это точно один и тот же тип для обеих пере-

менных. В этом смысле команды a=b и b=t не являются оригинальными.

Но в результате, к какому бы типу ни относились аргументы метода, мы их

значения «поменяли местами».

Как вызывается метод с параметром типа, показано в главном методе про-

граммы. Там создаются две целочисленные переменные, a и b, две текстовые

переменные, A и B, а также два объекта, objA и objB, класса MyClass. Пары этих

переменных по очереди передаются аргументами методу swap(), после чего

проверяется результат «обмена» значениями. Какое значение необходимо

передать методу в качестве параметра типа, мы указываем команде вызова

метода в угловых скобках после имени метода. Например, когда аргумента-

ми метода swap() являются целочисленные значения a и b, команда вызова

метода выглядит как swap(ref a,ref b). Это означает, что при выпол-

нении программного кода метода swap() все будет происходить так, как если

бы мы заменили X на int. Аналогично, команду swap(ref A,ref B)

Передача типа в качестве параметра           259

следует понимать так, что роль X играет тип string, а для команды swap

lass>(ref objA,ref objB) параметр типа X заменяется на значение MyClass.

Результат выполнения программы представлен на рис. 7.7.

Рис. 7.7.  Метод с параметром типа: результат выполнения программы

В принципе, при вызове метода с параметром типа значение параметра

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

пытка определить нужное значение для параметра типа по контексту

вызова метода (по типу его аргументов). Например, вместо команд

swap(ref a,ref b), swap(ref A,ref B) и swap(ref o bjA,ref objB) можно было бы использовать, соответственно, команды

swap(ref a,ref b), swap(ref A,ref B) и swap(ref objA,ref objB). Какое значе-

ние (какой тип) подставлять вместо параметра X, в этом случае можно

определить по типу аргументов, которые передаются методу swap().

Как  следствие, программный код  остается корректным. Например, в команде swap(ref a,ref b) аргументы типа int, а при описании их тип

был обозначен как X. Это означает, что X есть int. И так далее.

Метод может содержать несколько параметров типа. В этом случае

идентификаторы типов указываются через запятую в общих угловых

скобках после имени метода.

По тому же принципу создаются обобщенные классы — классы, содержа-

щие параметры типа. Только теперь в описании класса идентификаторы

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

объекта класса в угловых скобках указывают идентификаторы типа, кото-

рые следует использовать в качестве значений параметров типа. Такая же

процедура проделывается при объявлении объектных переменных. Ситуа-

цию иллюстрирует программный код в листинге 7.7.

Листинг 7.7.  Обобщенный класс (класс с параметрами типа) using System;

// Обобщенный класс с двумя параметрами типа:

class GClass{

// Открытое поле обобщенного типа X:

public X first;

продолжение

260

Глава 7. Методы и классы во всей красе

Листинг 7.7 (продолжение)

// Открытое поле обобщенного типа Y:

public Y second;

// Конструктор класса с двумя аргументами обобщенных типов: public GClass(X f,Y s){

first=f; // Присваивается значение первому полю

second=s; // Присваивается значение второму полю

}

// Открытый метод для отображения значения полей:

public void show(){

Console.WriteLine("Первое поле {0}, второе поле {1}.",first,second);

}

}

// Класс с главным методом программы:

class GClassDemo{

// Главный метод программы:

public static void Main(){

// Объектная переменная и объект

// обобщенного класса

// со значениями параметров типа int и char:

GClass A=new GClass(100,'A');

// Отображение полей объекта:

A.show();

// Объектная переменная обобщенного класса

// со значениями параметров типа string и string:

GClass B;

// Объект обобщенного класса

// со значениями параметров типа string и string:

B=new GClass("ПЕРВОЕ","ВТОРОЕ");

// Отображение полей объекта:

B.show();

// Ожидание нажатия клавиши Enter:

Console.ReadKey();

}

}

Мы объявляем обобщенный класс с заголовком class GClass. Угло-

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

параметра типа — X и Y. В самом классе описывается два поля — одно типа

X, а другое типа Y. Также у класса есть конструктор с двумя аргументами.

Первый аргумент конструктора имеет тип X и определяет значение перво-

го поля класса, а второй аргумент конструктора имеет тип Y и определяет

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

объекта класса.

Использование обобщенного типа данных           261

ПРИМЕЧАНИЕ Таким образом, неявно на типы X и Y накладывается небольшое ограни-

чение: они должны быть такими, чтобы переменные/объекты этих типов

можно было передавать аргументами методу Console.WriteLine().

В главном методе программы командой GClass A=new GClass(100,'A') создаются объектная переменная и объект обобщенного

класса GClass со значениями параметров типа int (для параметра X) и char (для параметра Y). Пара значений для параметров типов в угловых скобках

указывается после имени класса GClass как в части объявления объектной

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

обобщенными классами, процесс объявления объектной переменной и соз-

дание объекта для нее можно разнести во времени и пространстве. Так мы

и поступили, объявив командой GClass B объектную пере-

менную B обобщенного класса GClass со значениями параметров типа string (для X) и string (для Y). Создание объекта (с такими же значениями пара-

метров типа) и присваивание его в качестве значения объектной перемен-

ной выполняется командой B=new GClass("ПЕРВОЕ","ВТО -

РОЕ"). Проверка значений полей созданных объектов выполняется вызовом

метода show(). Результат выполнения программы представлен на рис. 7.8.

Рис. 7.8.  Обобщенный класс: результат выполнения программы

Разумеется, мы рассмотрели достаточно простой пример. Вместе с тем даже

он дает неплохое представление о том, насколько эффективным может

быть использование обобщенных классов и обобщенных методов, особенно

в комбинации с другими эффективными приемами программирования.

Использование обобщенного

типа данных

Эх, погубят тебя слишком широкие возможности.

Из к/ф «Айболит 66»

Особенность языка C# такова, что в вершине иерархии классов, как библи-

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

262

Глава 7. Методы и классы во всей красе

Причем это относится не только к объектным (или ссылочным) типам дан-

ных, но и к нессылочным типам (таким, например, как int или double).

Напомним,  что  название  класса  object  является  синонимом,  или

псевдонимом, класса System.Object.

Данное незначительное на первый взгляд обстоятельство имеет довольно

серьезные последствия, если вспомнить, что переменная базового типа мо-

жет ссылаться на объект производного типа. Более того, в C# есть так назы-

ваемая процедура приведения к объектному типу и извлечения значения из

объектного типа. Эта процедура дает возможность связать данные нессы-

лочного типа со ссылочным типом, то есть «упаковать» обычную перемен-

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

менная) присваивается переменной класса object. Для обратного преобра-

зования необходимо перед object-значением указать инструкцию явного

приведения типа (в круглых скобках идентификатор конечного типа).

В классе object объявляется виртуальный метод ToString(). Этот метод воз-

вращает в качестве результата текстовое значение и наследуется во всех клас-

сах. Более того, даже для базовых типов этот метод доступен. Его особенность

в том, что метод вызывается автоматически каждый раз, когда объект должен

быть преобразован в текстовое значение. Каждый раз, когда объект оказыва-

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

передан аргументом методу Console.WriteLine()), автоматически вызывается

метод ToString(), переопределенный в классе объекта или унаследованный

этим классом. Поэтому если мы в классе переопределим метод ToString(), то

в принципе объект можно будет использовать в качестве текста.

Для явного преобразования объекта в текст из объекта можно вы-

звать метод ToString().

Здесь мы рассмотрим небольшой пример того, как могут использоваться

перечисленные выше особенности при написании программных кодов. Ис-

следуем программный код, представленный в листинге 7.8.

Листинг 7.8.  Использование класса object

using System;

// Класс с использованием object-типа:

class OClass{

// Открытое поле класса object:

Использование обобщенного типа данных           263

public object one;

// Открытое поле класса object:

public object two;

// Конструктор класса с двумя аргументами:

public OClass(object one, object two) {

this.one=one; // Значение первого поля

this.two=two; // Значение второго поля

}

// Метод для отображения значения полей объекта:

public void show(){

// Неявно используем переопределенный метод ToString():

Console.WriteLine(this); // Аргументом указан объект вызова

}

// Переопределение метода ToString() для класса OClass:

public override string ToString(){

// Текстовый "эквивалент" объекта:

return "Первый аргумент "+one+". Второй аргумент "+two+".";

}

}

// Класс с главным методом программы:

class ObjectTypeDemo{

// Главный метод программы:

public static void Main(){

// Объектная переменная класса OClass:

OClass obj;

// Создание объекта класса OClass c полями

// целочисленного и символьного типа:

obj=new OClass(10,'A');

// Проверяем "содержимое" объекта:

obj.show();

// Создание объекта класса OClass c двумя

// текстовыми полями:

obj=new OClass("ПЕРВЫЙ","ВТОРОЙ");

// Проверяем "содержимое" объекта:

obj.show();

// Текстовому полю присваиваем

// целочисленное значение:

obj.one=1;

// Проверяем "содержимое" объекта:

obj.show();

// Создаем и инициализируем массив

// объектов класса object.

// Значения элементов - самые разные:

object[] m=new object[]{"Элемент № 1",2,'Ы',new OClass(1.23,100)}; продолжение

264

Глава 7. Методы и классы во всей красе

Листинг 7.8 (продолжение)

// Отображаем элементы массива:

for(int i=0;i

Console.WriteLine(i+1+": "+m[i]);

}

// Ожидание нажатия клавиши:

Console.ReadKey();

}

}

Как ни странно, программный код не только компилируется, но еще и до-

вольно неплохо выполняется. На рис. 7.9 представлен результат выполне-

ния программы.

Рис. 7.9.  Использование класса object: результат выполнения программы

Нам остается понять, почему написанный нами код является корректным.

Все «хитрости» этого кода спрятаны в классе OClass. Структура класса до-

статочно простая. У него есть два поля с названиями one и two. Оба поля

указаны как объекты класса object. Есть у класса конструктор с двумя

аргументами. Код конструктора тривиальный — аргументы конструктора

присваиваются в качестве значений полям объекта.

Более примечательными являются методы show() и ToString(). Мы нач-

нем анализ именно с метода ToString(), поскольку в методе show() просто

пожинаются плоды переопределения метода ToString(). Итак, в заголов-

ке метода мы видим атрибуты public (метод, открытый по определению), override (имеет место переопределение метода) и string (метод в качестве

результата возвращает текстовое значение). Аргументов у метода нет. Это

фактически стандартная шапка метода при его переопределении. Здесь мы

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

будет внутри метода. В рассматриваемом примере там всего одна команда

"Первый аргумент "+one+". Второй аргумент "+two+".", которой в качестве

результата возвращается текстовая строка, в которую «вмонтированы»

ссылки на поля объекта (в некотором смысле их можно рассматривать как

значения полей — но это только для нессылочных типов). Именно такая

строка будет использоваться каждый раз, когда объект класса OClass ока-

жется в «текстовом» месте.

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

Первая проверка на надежность метода выполняется в методе show(), в теле

которого мы поместили всего одну команду — Console.WriteLine(this). Здесь

аргументом метода Console.WriteLine() указана ссылка на объект вызова — то

есть на объект класса OClass. Поэтому эффект такой, как если бы аргументом

методу Console.WriteLine() передавался результат вызова метода ToString().

С кодом класса OClass мы ситуацию разъяснили. Теперь посмотрим, что

происходит в главном методе программы.

Командой OClass obj мы объявляем объектную переменную obj класса

OClass, и в этом нет пока ничего необычного. Небольшая экзотика начи-

нается, когда мы встречаем команду obj=new OClass(10,'A'). Особенность

команды — в аргументах конструктора. Они не только разного типа (целое

число и символ), но еще и формально не относятся к классу object. Но

это только формально. Поскольку класс object является базовым для всех

классов и нессылочных типов, то аргументы конструктора неявно преоб-

разуются в тип object. Аналогичная ситуация имеет место при выполне-

нии команды obj=new OClass("ПЕРВЫЙ","ВТОРОЙ"), здесь принцип тот же, но

только аргументы конструктора — оба текстовые. Более того, корректной

является и obj.one=1. Здесь полю, которое до этого имело фактически тек-

стовое значение, в качестве нового значение присваивается целое число.

Каждый раз результат манипуляций с объектами мы проверяем с помощью

метода show(), который вызывается из объекта obj командой obj.show().

Но на этом наше исследование могущества класса object не закан-

чивается. Командой object[] m=new object[]{"Элемент № 1",2,'Ы', new OClass(1.23,100)} мы создаем массив объектов класса object, причем

инициализация массива выполняется значениями самых разных типов: текстовым значением, числом, символом и объектом класса OClass (у ко-

торого два «числовых» поля: действительное и целое число). С помощью

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

этом, когда очередь доходит до отображения «значения» последнего эле-

мента массива-объекта класса OClass, в игру вновь вступает переопреде-

ленный метод ToString() этого класса.

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

— Простите, часовню тоже я развалил?

— Нет, это было до вас, в XIV веке.

Из к/ф «Кавказская пленница»

С обработкой исключительных ситуаций мы уже встречались. Здесь под-

ведем под этот процесс некоторую теоретическую основу. Но сначала

266

Глава 7. Методы и классы во всей красе

немного освежим память. Для нас важными будут следующие обстоятель-

ства.

 Если при выполнении программного кода происходит ошибка, авто-

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

описание ошибки и имеет ряд специфических свойств.

 Этот объект «вбрасывается» в программу, которая вызвала ошибку. Если

объект ошибки не обрабатывается, программа экстренно (в «аварийном»

режиме) завершает работу.

 Чтобы программа при возникновении ошибки (исключения или исклю-

чительной ситуации) работу не завершала, исключительная ситуация

должна быть «обработана».

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

блок, а код, который выполняется при обработке ошибки, помещается

в catch-блок.

ПРИМЕЧАНИЕ Собственно, мы уже видели (в минимальном объеме, правда), как

работает try-catch конструкция. Теперь настало время поближе по-

знакомиться с объектами ошибок, или исключениями.

Что касается объекта, который создается при возникновении ошибки, то уже в силу того обстоятельства, что это объект, он должен относиться

к какому-то классу. Для всех основных ошибок, которые в принципе могут

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

классы описывают всевозможные типы ошибок. При возникновении опре-

деленной ошибки на основе класса, который соответствует этой ошибке, создается объект. Классы ошибок не разрозненные. У них строгая иерар-

хия, в вершине которой находится класс Exception, который описан в про-

странстве System. У класса Exception имеются подклассы SystemException и ApplicationException. Классы для основных «стандартных» ошибок (или ис-

ключений) относятся к ветке иерархии наследования класса SystemException.

Чтобы понять, как эффективно использовать классы исключений, разберем-

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

через систему try­catch блоков. Достаточно общий шаблон использования

соответствующей «пожарной» конструкции выглядит примерно так:

// Начальный try-блок:

try{

// Контролируемый программный код

}

// Первый catch-блок:

catch(Класс_исключения_1 объект_1){

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

// Программный код на случай возникновения

// ошибки типа Класс_исключения_1

}

// Второй catch-блок:

catch(Класс_исключения_2 объект_2){

// Программный код на случай возникновения

// ошибки типа Класс_исключения_2

}

...

// N-й catch-блок:

catch(Класс_исключения_N объект_N){

// Программный код на случай возникновения

// ошибки типа Класс_исключения_N

}

// Следующая команда

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

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

сле ключевого слова try. После try-блока следует несколько catch-блоков, обычно тоже с программным кодом. Количество блоков не регламенти-

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

в catch-блоках «вступает в игру» только в том случае, если при выполнении

программного кода в try-блоке возникла ошибка. Если ошибка не возник-

ла, что именно содержится в catch-блоках — непринципиально, поскольку

этот код не выполняется, а управление передается той команде, которая на-

ходится после всей try­catch конструкции. Все намного интереснее, если

ошибка возникла. В этом случае, как мы знаем, в зависимости от типа воз-

никшей ошибки создается объект, а дальше начинается последовательный

перебор catch-блоков. Обычно catch-блоки имеют нечто наподобие аргу-

мента — в круглых скобках после ключевого слова catch указывается имя

класса ошибки и, по желанию, объектная переменная, которая играет роль

аргумента. Эти catch-блоки один за другим проверяются на предмет того, совпадает ли класс объекта ошибки с тем классом, что указан в круглых

скобках после ключевого слова catch. Если совпадения нет, то проверяет-

ся следующий блок, и т. д. Как только совпадение найдено, начинает вы-

полняться программный код соответствующего catch-блока. При этом если

кроме типа ошибки в скобках после ключевого слова catch указана и объ-

ектная переменная, то этой объектной переменной в качестве значения при-

сваивается ссылка на объект ошибки. Но нередко обработка ошибки вы-

полняется без непосредственного обращения к объекту ошибки.

В случае, когда при переборе catch-блоков совпадение не найдено, ошиб-

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

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

268

Глава 7. Методы и классы во всей красе

концов, исключение не будет перехвачено и обработано, программа завер-

шит работу в аварийном режиме.

Если в catch-блоке не указать тип исключения, такой catch-блок будет

перехватывать все ошибки. Обычно такой блок добавляют в конце

конструкции try-catch. В случае если нужно, чтобы при завершении

try-блока какой-то код выполнялся при любых раскладах, в конструк-

цию try-catch можно добавить блок finally.

Еще одно важное замечание касается способа поиска совпадений

типов ошибок при переборе catch-блоков. Важно знать, что если тип

(класс) ошибки является производным классом от класса ошибки, указанного  в  catch-блоке,  то  считается,  что  имеет  место  совпаде-

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

Exception, то перехватываться будет практически все.

Настал момент рассмотреть небольшой пример. Обратимся к программно-

му коду, представленному в листинге 7.9.

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

using System;

// Класс с главным методом программы:

class ECatchDemo{

// Главный метод с обработкой

// исключительных ситуаций:

public static void Main(){

// Объект rnd класса Random для

// генерирования случайных чисел:

Random rnd=new Random();

// Целочисленный массив из трех элементов:

int[] n=new int[3];

// Целочисленные переменные:

int i,k,a;

// Оператор цикла:

for(i=1;i<=20;i++){

k=rnd.Next(0,4); // Случайное целое число

// от 0 до 3 включительно

a=rnd.Next(0,4); // Случайное целое число

// от 0 до 3 включительно

// Блок контроля программного кода:

try{

// Возможна ошибка: деление на нуль

// или выход за пределы массива:

n[k]=6/a; // Элементу массива

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

// присваивается значение

// Команда выполняется, если выше

// не произошла ошибка:

Console.WriteLine("Индекс {0}. Значение {1}.",k,n[k]);

}

// Перехват ошибки выхода за пределы массива:

catch(IndexOutOfRangeException){

Console.WriteLine("Выход за пределы массива.");

}

// Перехват ошибки деления на нуль:

catch(DivideByZeroException){

Console.WriteLine("Деление на нуль.");

}

}

// Ожидание нажатия клавиши:

Console.ReadKey();

}

}

Программа простая и бесполезная. В главном методе программы создает-

ся целочисленный массив из трех элементов. Индексы элементов массива, таким образом, могут изменяться от 0 до 2 включительно. Запускается опе-

ратор из 20 циклов, и за каждый цикл выполняются некоторые нехитрые

действия: генерируются два целых случайных числа в диапазоне от 0 до 3

включительно.

Для генерирования случайных чисел мы создаем объект rnd библио-

течного класса Random. В классе прописан метод Next(), который

позволяет генерировать случайные целые числа. Результатом выра-

жения вида rnd.Next(m,M+1) является случайное число в диапазоне

от m до M.

Одно случайное число используется в качестве индекса элемента массива, а второе фигурирует в знаменателе в операции присваивания значения эле-

менту массива. Помимо штатных ситуаций, когда элементу с легитимным

индексом присваивается значение 6, 3 или 2, возможны две нештатные си-

туации: деление на нуль и выход индекса за пределы массива. Поэтому фраг-

мент кода, который может сгенерировать нам неприятность (а это команда

присваивания значения элементу массива с примкнувшей к ней командой

вывода результата на экран), помещается в try-блок. На случай возникно-

вения ошибок после try-блока есть два catch-блока. Ошибке деления на

ноль соответствует класс DivideByZeroException. Ошибке выхода индекса

за пределы массива соответствует класс IndexOutOfBoundsException. Со-

ответствующие классы указываются в круглых скобках после ключевого

270

Глава 7. Методы и классы во всей красе

слова catch. Поскольку сам объект ошибки нам в данном случае не нужен, объектные переменные для этих классов мы не указываем.

В случае если возникает ошибка, выполнение команд try-блока прекра-

щается и выполняется код одного из catch-блоков. Затем начинает вы-

полняться следующий цикл внешнего в try­catch конструкции оператора

цикла. Результат выполнения программы показан на рис. 7.10.

Рис. 7.10.  Возможный результат выполнения программы с перехватом

исключений деления на ноль и выхода за пределы массива

Следует иметь в виду, что поскольку здесь мы используем случайные чис-

ла, то и результат выполнения программы также является случайным. По-

этому от запуска к запуску картинка будет меняться.

Хотя это может показаться странным, но можно генерировать исключе-

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

происходить ошибка, когда ее на самом деле нет. Мы не создаем ошибку, мы создаем иллюзию ошибки. Для искусственного генерирования ошибки

используют инструкцию throw, после которой указывается объект генери-

руемой ошибки. Поскольку словами это объяснять все равно бесполезно, изучим программный код в листинге 7.10. Это несколько модифицирован-

ный программный код из предыдущего примера.

Листинг 7.10.  Искусственное генерирование ошибки

using System;

class ThrowDemo{

// Главный метод с обработкой

// исключительных ситуаций:

public static void Main(){

// Объект rnd класса Random для

// генерирования случайных чисел:

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

Random rnd=new Random();

// Целочисленный массив из трех элементов:

int[] n=new int[3];

// Целочисленные переменные:

int i,k,a;

// Оператор цикла:

for(i=1;i<=20;i++){

k=rnd.Next(0,4); // Случайное целое число

// от 0 до 3 включительно

a=rnd.Next(0,4); // Случайное целое число

// от 0 до 3 включительно

// Блок контроля программного кода:

try{

// Генерирование искусственной ошибки:

if(a==0&&k>n.Length­1) throw new Exception();

// Возможна ошибка: деление на нуль

// или выход за пределы массива:

n[k]=6/a; // Элементу массива

// присваивается значение

// Команда выполняется, если выше

// не произошла ошибка:

Console.WriteLine("Индекс {0}. Значение {1}.",k,n[k]);

}

// Перехват ошибки выхода за пределы массива:

catch(IndexOutOfRangeException){

Console.WriteLine("Выход за пределы массива.");

}

// Перехват ошибки деления на нуль:

catch(DivideByZeroException){

Console.WriteLine("Деление на нуль.");

}

// Перехват "двойной" ошибки:

catch(Exception){

Console.WriteLine("Двойная ошибка!");

}

}

// Ожидание нажатия клавиши:

Console.ReadKey();

}

}

По сравнению с предыдущим примером (см. листинг 7.9) изменения мини-

мальные. А именно, в начале try-блока добавлена команда if(a==0&&k>n.

Length­1) throw new Exception() для генерирования искусственной ошиб-

ки, и появился еще один, третий, catch-блок с аргументом-классом Exception.

Что это дает? Если ситуация такова, что случайное число, обозначающее ин-

декс элемента массива, выходит за допустимые пределы, а случайное число,

272

Глава 7. Методы и классы во всей красе

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

бы «двойную» ошибку. Эту «двойную» ошибку мы хотим обрабатывать по

особым правилам. Поэтому, если выполнено условие a==0&&k>n.Length­1, ко-

мандой throw new Exception() генерируется исключение класса Exception.

ПРИМЕЧАНИЕ После инструкции throw мы указали анонимный объект new Exception() класса Exception.

Для обработки этой исключительной ситуации в третьем catch-блоке есть

команда Console.WriteLine("Двойная ошибка!"). Причем важно, чтобы блок

для обработки исключения класса Exception был последним. Если его, на-

пример, гипотетически поставить первым, то он перехватывал бы и две

другие ошибки — деление на нуль и выход за пределы массива. Это на-

столько трагичная ситуация, что ее даже компилятор не допустит. Резуль-

тат (возможный) выполнения программы представлен на рис. 7.11.

Рис. 7.11.  Генерирование искусственной ошибки: возможный результат выполнения

программы

ПРИМЕЧАНИЕ Надо понимать, что сообщение Двойная ошибка! — редкий гость

в консольном окне. Если случайные числа генерируются с равной

вероятностью, то вероятность для каждого из событий «деление на

нуль» и «выход за пределы массива» составляет 1/4. Вероятность

того, что произойдет хоть одно из этих событий, равна 7/16. А вероят-

ность того, что произойдут оба события, равняется 1/16. Математиче-

ское ожидание (оценка для среднего количества появления двойной

ошибки) для 20 запусков цикла составляет 20/16=1,25, то есть чуть

больше единицы.

Многопоточное программирование           273

Многопоточное программирование

Куда? Эй, куда же вы все-то разбежались?

Кто-нибудь, держите меня!

Из к/ф «Айболит 66»

Еще одна полезная возможность, с которой мы познакомимся (достаточно

кратко) — это возможность создавать в программе потоки. Потоками назы-

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

Такое программирование называется многопоточным программировани-

ем. В C# многопоточность встроенная. Это означает, что язык обладает на-

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

потоков исключительно программными средствами C#.

Элементарная логика подсказывает, что если программа выполняется, то

по крайней мере один поток имеется. Этот поток обычно называют глав-

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

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

нения программы.

Понятно, что тема эта перспективная и очень обширная. Мы, в силу объек-

тивных причин, освоим лишь азы. Другими словами, наша задача состоит

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

Для этого нам понадобятся классы (точнее, мы будем использовать лишь

один класс), которые позволяют создавать многопоточные программы.

Классы описаны в пространстве имен System.Threading. Поэтому програм-

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

содержать инструкцию using System.Threading. Тот класс, который инте-

ресует нас, называется Thread. Он важен тем, что создание потока как та-

кового означает создание объекта класса Thread. Поэтому нам важно знать

побольше об этом классе.

Класс Thread не может быть базовым. Он объявлен с ключевым сло-

вом sealed, а это означает, что на основе класса нельзя создавать

производные классы.

После того как объект класса Thread создан (объект потока), нужно запу-

стить поток. Запуск потока выполняется вызовом метода Start() из объ-

екта потока. В результате поток начинает выполняться. Но поток — это, по большому счету, последовательность команд. Откуда им взяться? Из

метода, который запускается вследствие вызова метода Start(). Пытли-

вый читатель может спросить: а откуда известно, какой следует запускать

274

Глава 7. Методы и классы во всей красе

метод при вызове метода Start()? Это правильный вопрос. Правильный

ответ такой: при создании объекта потока, то есть объекта класса Thread, аргументом конструктору этого класса передается экземпляр делегата того

метода, который запускается при вызове метода Start(). Делегат называ-

ется ThreadStart, а его экземпляры могут ссылаться на открытые методы, которые не имеют аргументов и не возвращают результат. Вся эта «кухня»

может быть реализована совершенно разными способами. Но есть некие

ключевые моменты:

1. В наличии должен быть метод, открытый, без аргументов и не возвра-

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

ется с выполнением потока. Те команды, которые есть в методе, и будут

выполняться в рамках потока.

2. Должен быть создан экземпляр делегата ThreadStart, ссылающийся на

указанный выше метод.

3. На основе экземпляра делегата ThreadStart создается объект класса

Thread. Экземпляр делегата передается конструктору класса Tread в ка-

честве аргумента.

4. Из объекта класса Thread следует запустить метод Start().

ПРИМЕЧАНИЕ Программа, которую мы рассматриваем, имеет прямое отношение

к  большому  спорту.  В  ней  мы  пытаемся  смоделировать  методами

многопоточного программирования забег на марафонскую дистан-

цию (42 195 метров) двух пушистых спортсменов: Зайца и Лисы. Оба

спортсмена одновременно начинают забег и двигаются по дистанции

со  средней  скоростью  30  км/ч,  что  составляет  500  м/мин.  Такая

скорость более-менее отвечает реальным возможностям пушистой

братии. Для сравнения — заяц-русак, по некоторым данным, может

развивать скорость до 60 км/ч.

Программа, как отмечалось, предназначена для имитации такого за-

бега. Для этого в программе создается и запускается два потока (не

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

Имитация выполняется так. Для каждого потока выделена специаль-

ная целочисленная переменная с начальным нулевым значением. Эта

переменная  определяет  расстояние,  которое  пробежал  спортсмен.

В каждом потоке через определенные промежутки времени значение

этой переменной увеличивается на случайное число. Побеждает тот из

спортсменов, кто быстрее придет к финишу — то есть чья «переменная

пройденного пути» первая достигнет марафонского значения 42 195.

Оба вспомогательных потока запускаются из главного потока. Через

одинаковые промежутки времени главный поток «считывает» текущее

значение переменных, которые определяют пройденный спортсменами

путь. Соответствующая информация отображается в консольном окне.

Многопоточное программирование           275

Рассмотрим программный код в листинге 7.11, в котором вся эта схема

и реализована.

Листинг 7.11.  Программа с несколькими потоками

using System;

using System.Threading;

// Все происходит в одном классе:

class Marathon{

// Марафонское расстояние:

const int Dist=42195;

// "Путь" Зайца:

private static int HareDist;

// "Путь" Лисы:

private static int FoxDist;

// Метод для потока "забег Лисы":

public static void goFox(){

FoxDist=0; // Начальное значение "пути" Лисы

Random rnd=new Random(); // Будем генерировать

// случайные числа

// Лиса ушла в забег:

Console.WriteLine("Лиса стартовала!");

do{

Thread.Sleep(20); // Небольшая задержка

// Рывок после отдыха:

FoxDist+=rnd.Next(200)+1;

}while(FoxDist

// Да, это он:

Console.WriteLine("Лиса финишировала!");

}

// Метод для потока "забег Зайца":

public static void goHare(){

HareDist=0; // Заяц на старте

Random rnd=new Random(); // Класс random - двигатель прогресса

// Заяц ушел в отрыв:

Console.WriteLine("Заяц стартовал!");

do{

Thread.Sleep(10); // Небольшой отдых

HareDist+=rnd.Next(100)+1; // Небольшой рывок

}while(HareDist

// Вот он:

Console.WriteLine("Заяц финишировал!");

}

продолжение

276

Глава 7. Методы и классы во всей красе

Листинг 7.11 (продолжение)

// Главный метод программы (главный поток):

public static void Main(){

// Готовим секундомер:

int count=0;

// "Служба информации":

string txt="-я минута: Заяц пробежал {0} метров,

Лиса - {1} метров.";

// Объектные переменные для потоков:

Thread Hare,Fox; // Каждому спортсмену - по дорожке!

// Экземпляры делегатов для передачи в потоки:

ThreadStart hare,fox;

// Экземплярам делегатов присваиваются

// значения:

hare=goHare; // Для потока "забег Зайца"

fox=goFox; // Для потока "забег Лисы"

// Создание объекта для потока Зайца:

Hare=new Thread(hare);

// Создание объекта для потока Лисы:

Fox=new Thread(fox);

// На старт, внимание, марш!

Console.WriteLine("Мы начинаем марафон!");

Hare.Start(); // Первый пошел!

Fox.Start(); // Второй пошел!

do{

count+=5; // Интервал в "минутах"

Thread.Sleep(500); // Даем время разогнаться

// Снимаем звериные "показания":

Console.WriteLine(count+txt,HareDist,FoxDist);

}while(Hare.IsAlive||Fox.IsAlive); // Пока хоть кто-то бежит

// Главный олимпийский принцип:

Console.WriteLine("Главное не победа, а участие!");

// Наслаждаемся результатом:

Console.ReadKey();

}

}

Весь процесс реализован в одном классе Marathon. В нем мы определяем

поле-константу Dist со значением 42195 (марафонская дистанция в ме-

трах), а также два статических целочисленных поля HareDist (расстояние, преодоленное Зайцем) и FoxDist (расстояние, преодоленное Лисой). Ме-

тод goFox() не возвращает результат и не имеет аргументов. Этот метод

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

ды в теле метода — это команды, которые выполняются при выполнении

потока. В теле метода переменной FoxDist присваивается начальное ну-

левое значение и создается объект rnd класса Random (для генерирования

Многопоточное программирование           277

случайных чисел). Затем командой Console.WriteLine("Лиса стартовала!") в консоль выводится сообщение о том, что спортсмен вступил в борьбу.

Но все самое интересное происходит в операторе do­while(). Командой

Thread.Sleep(20) выполняется задержка в 20 миллисекунд. После такой

вынужденной задержки командой FoxDist+=rnd.Next(200)+1 значение пе-

ременной FoxDist увеличивается на случайное число от 1 до 200. Оператор

цикла выполняется, пока переменная FoxDist меньше марафонской кон-

станты Dist (условие FoxDist

ния оператора цикла, командой Console.WriteLine("Лиса финишировала!") в консоль выводится сообщение с оптимистичным содержанием.

В классе Thread описан статический метод Sleep(). Если вызывать

этот метод с целочисленным аргументом, то выполнение потока, из

которого вызывается метод, будет приостановлено на время (в мил-

лисекундах), указанное аргументом метода. К помощи метода Thread.

Sleep() мы будем прибегать неоднократно.

Метод goHare(), который выполняется для второго потока («поток Зай-

ца»), от метода goFox() принципиально отличается лишь тем, что задержка

по времени там в 2 раза меньше (10 миллисекунд) и в 2 раза меньше диа-

пазон генерирования случайных чисел (от 1 до 100). Таким образом, наш

Заяц прыгает чаще, но на меньшие расстояния.

Но все это были предварительные размышления о том, какой поток как

выполняется. Эти самые потоки где-то надо создать и как-то надо запу-

стить. Подходящий в этом смысле метод — главный метод программы.

Локальная целочисленная переменная count, инициализированная с на-

чальным нулевым значением, послужит «секундомером» — мы с ее по-

мощью будем отмечать моменты времени, в которые производятся за-

меры преодоленных спортсменами расстояний. Вспомогательным целям

служит и текстовая строка txt — она содержит текст, на основе которого

будет формироваться выводимое в консоль сообщение о результатах кон-

троля.

Командой Thread Hare,Fox мы объявляем две объектные переменные, Hare и Fox, класса Thread. Попозже в эти переменные мы запишем ссыл-

ки на соответствующие объекты потоков. Но предварительно эти объек-

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

объявить экземпляры делегата ThreadStart. Для этой цели служит коман-

да ThreadStart hare,fox. Командами hare=goHare и fox=goFox экземплярам

делегатов присваиваем в качестве значений ссылки на соответствующие

методы. Теперь можно создавать объекты потоков, что мы и делаем с помо-

щью команд Hare=new Thread(hare) и Fox=new Thread(fox). Осталось только

278

Глава 7. Методы и классы во всей красе

запустить потоки. Предваряя это, мы командой Console.Write Line("Мы на­

чинаем марафон!") выводим в консоль сообщение угрожающего свойства, и командами Hare.Start() и Fox.Start() последовательно запускаем два

потока.

Здесь  есть  важный  идеологический  момент.  После  того  как  поток

запущен (например, командой Hare.Start()), он начинает жить своей

почти независимой жизнью. А метод Main() продолжает выполняться

своим чередом.

В методе Main() тем временем запускается оператор цикла, в котором за

каждый цикл переменная-счетчик count увеличивает с дискретностью 5.

Командой Thread.Sleep(500) выполняется задержка главного потока (того

потока, в котором метод Main() выполняется) на 500 миллисекунд, после

чего командой Console.WriteLine(count+txt,HareDist,FoxDist) отобража-

ется сообщение с информацией о том, какая зверушка сколько успела про-

бежать. Соответствующие значения считываются из переменных HareDist и FoxDist. В качестве условия продолжения оператора цикла указана кон-

струкция Hare.IsAlive||Fox.IsAlive. В ней из объектов потока запрашива-

ется свойство IsAlive. Свойство возвращает логическое значение true, если

соответствующий поток выполняется. Если поток уже завершен, возвра-

щается значение false. Поэтому значением выражения Hare.IsAlive||Fox.

IsAlive является true, если хотя бы один из потоков выполняется. Таким

образом, оператор цикла в главном завершается только после завершения

выполнения потоков для объектов Hare и Fox. В конце выполнения про-

граммы командой Console.WriteLine("Главное не победа, а участие!") на

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

На рис. 7.12 показан возможный результат выполнения программы.

В данном случае победила Лиса, хотя она и стартовала второй.

ПРИМЕЧАНИЕ Несложно заметить, что сообщение о положении спортсменов на по-

следней секунде появляется после сообщения о приходе к финишу.

Причина в следующем. Сообщение о приходе к финишу отображается

из потока, который завершается немного раньше завершения опера-

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

выполняться до того, как оба потока завершились, но за счет искус-

ственной временной задержки вывод информации происходит после

вывода сообщений о завершении потоков. Вообще, синхронизация

работы потоков может быть темой отдельной книги. Здесь нам до-

статочно понять ее значимость.

Многопоточное программирование           279

Рис. 7.12.  Возможные результаты «зверского марафона»

Мы рассмотрели очень простой пример, связанный с использованием по-

токов. Это действительно очень мощное и гибкое средство программиро-

вания. Но, увы, полностью осветить эту тему здесь мы все равно не смо-

жем. Кроме того, следует иметь в виду, что предложенный выше способ

организации потоков хотя и правильный, но не очень «классический». Это

и не плохо, и не хорошо — просто по-другому. Тем не менее, если читате-

лю удалось уловить основную суть, или идею, многопоточности, то можно

считать, что свою задачу пример выполнил.

Приложение

с графическим

интерфейсом:

учебный проект

Форму будете создавать под моим

личным контролем. Форме сегодня

придается большое ... содержание.

Из к/ф «Чародеи»

Эта глава всецело посвящена одному-единственному примеру о том, как

создавать приложения с графическим интерфейсом, в котором не только

одни кнопки с текстовыми метками, но и некоторые другие графические

элементы. Справедливости ради следует отметить, что создание графиче-

ского интерфейса представляется делом малоперспективным в том смыс-

ле, что процесс, по своей сути, достаточно шаблонный и с точки зрения

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

граммирования C# как раз и хорош тем, что с его помощью достаточно

легко создаются приложения с графическим интерфейсом. Поэтому не на-

писать в книге по C# о том, как создать форму с кнопочками, пиктограмм-

ками, переключателями и другими деликатесами — все равно что объявить

войну, а военных об этом не предупредить. Но это еще не все. В этой главе

мы несколько изменим базовый подход и, в некотором смысле, предоста-

вим читателя самому себе. Читатель сможет найти, конечно же, полный

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

Многопоточное программирование           281

в основу программы, а также демонстрацию (в разумных пределах) функ-

циональных возможностей программы. Также в главе описаны наиболее

трудно воспринимаемые моменты и на общем уровне базовые алгоритмы.

Есть и краткая справка по способам работы с графическими элементами.

Тем не менее материал главы предполагает, что читатель затратит серьез-

ное время на «самоподготовку» и детальный разбор программного кода.

ПРИМЕЧАНИЕ Важно помнить, что пример все-таки учебный. Поэтому во многих

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

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

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

спектр  возможностей  и  гибкость  языка  C#.  Опять  же,  в  примере

упор делается на вопрос «как сделать?», а не на вопрос «зачем это

делать?». Поэтому глубокого философского смысла в предназначении

описываемой далее программы искать не стоит.

Что касается самого примера, то мы пытаемся создать программу, в ре-

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

окне есть область с постоянным текстовым значением (реализуется через

текстовую метку). Для отображения текстового содержимого можно при-

менять различные шрифты. Настройка параметров шрифта выполняется

непосредственно в окне формы. Можно выбрать тип шрифта (Arial, Times и Courier), стиль шрифта (Жирный и Курсив) и размер шрифта (в диапазоне от

10 до 20). Утилита выбора типа шрифта реализуется через группу из трех

переключателей (радиокнопок). Выбор стиля шрифта выполняется с по-

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

На форме имеется две кнопки: одна — кнопка применения настроек, дру-

гая кнопка позволяет завершить работу приложения.

При выполнении настроек они автоматически в силу не вступают.

Для их применения необходимо щелкнуть на специальной кнопке.

Исключение составляют переключатели выбора типа шрифта — из-

менение  положения  переключателя  приводит  к  автоматическому

применению настроек.

Также у формы есть главное меню, которое дублирует выполнение всех

перечисленных операций.

Перед тем, как приступить к анализу программного кода и всего, что с ним

связано, сделаем краткий экскурс в мир графических элементов оконных

форм.

282

Глава 8. Приложение с графическим интерфейсом: учебный проект

Общие сведения о графических

элементах

Отлично, отлично!

Простенько, и со вкусом!

Из к/ф «Бриллиантовая рука»

Мы рассмотрим и обсудим только те элементы и классы, которые имеют

непосредственное отношение к нашей задаче. С кнопками и метками мы

уже знакомы. Кнопкам соответствует класс Button, а для реализации меток

используют класс Label. Кроме этого, нам понадобятся кнопки-опции (эле-

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

реализуются через класс CheckBox. Для ввода размера шрифта нам понадо-

бится текстовое поле — объект класса TextBox. Кнопки-переключатели (или

радиокнопки) реализуются в виде объектов класса RadioButton. Но здесь

есть один тонкий момент. Дело в том, что такие кнопки-переключатели ис-

пользуют для организации групп переключателей. В каждой группе только

один и только один переключатель может быть выделен (или установлен).

Поэтому радиокнопки мало добавить в форму — их еще нужно сгруппиро-

вать. Для группы кнопок создается объект класса GroupBox.

Главное меню формы — это меню, которое находится в верхней части фор-

мы под строкой названия. А еще главное меню формы — это объект клас-

са MainMenu. Отдельные пункты меню, которые входят в состав главного

меню, являются объектами класса MenuItem. Команды или подменю, из

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

ектами класса MenuItem. Меню создается путем добавления подпунктов

к пунктам меню. У объектов класса MenuItem имеется два полезных в на-

шем деле свойства. Свойство Text предсказуемым образом возвращает тек-

стовое название пункта меню. Свойство Index возвращает индекс пункта

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

Мы достаточно часто будем использовать свойство Text для самых

разных объектов. Понятно, что многое зависит от объекта, но в прин-

ципе это свойство определяет текст, который отображается в области

соответствующего элемента.

Для «вкладывания» подпункта меню/команды в пункт меню из коллекции

MenuItems объекта «внешнего» пункта меню (контейнера) вызывается ме-

тод Add(), аргументом которого указывается объект добавляемого пункта

Общие сведения о графических элементах           283

меню или команды. Чтобы связать главное меню с формой, необходимо

свойству Menu формы в качестве значения присвоить ссылку на объект

главного меню.

Достаточно полезный метод SetBounds() позволяет задать положение и раз-

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

ляют координаты левого верхнего угла элемента по отношению к своему

контейнеру (элементу, который содержит другие элементы). Два других

аргумента метода — это линейные размеры элемента (ширина и высота).

Полезнейшее свойство элементов — свойство Font. В качестве значения

свойству присваивается объект одноименного класса, который и определя-

ет шрифт, применяемый для отображения текстовых надписей в области

элемента. В программе это свойство задается для всей формы и для метки

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

если мы задаем шрифт формы, то мы автоматически задаем его для всех

элементов. Для тех элементов, которые должны иметь «особый» шрифт, объект с параметрами шрифта присваивается в качестве свойства Font со-

ответствующего графического элемента. Для создания объекта класса Font мы будем использовать конструктор с тремя аргументами. Первым аргу-

ментом указывается текстовое название шрифта. Второй аргумент — это

размер шрифта. Третий аргумент — константа перечисления FontStyle.

В частности, нас будут интересовать значения FontStyle.Regular (обыч-

ный шрифт), FontStyle.Bold (жирный шрифт) и FontStyle.Italic (кур-

сив). Особенность значений перечисления FontStyle такова, что если по-

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

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

Bold|FontStyle.Italic. С другой стороны, добавление жирного стиля или

курсива к обычному шрифту означает применение, соответственно, жир-

ного стиля или курсива. На этой особенности базируются некоторые не-

сложные вычисления при обработке настроек в окне формы.

В качестве текстовых названий шрифтов мы используем названия

«Arial»,  «Times»  и  «Courier»,  а  в  результате,  скорее  всего,  будут

применяться шрифты Arial, Times New Roman и Courier New соот-

ветственно. Вообще же в таких вопросах лучше отталкиваться от

системных параметров — в данном случае списка установленных

шрифтов.

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

284

Глава 8. Приложение с графическим интерфейсом: учебный проект

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

используем событие CheckedChanged, которое происходит при изменении

состояния переключателя. Для опций полезным событием-членом будет

Checked, которое позволяет определить, установлена опция или нет. Что

касается обработчиков событий, то, напомним, это должны быть мето-

ды, не возвращающие результат, с двумя аргументами: объектом класса

Object, который определяет вызвавший событие объект, и объектом клас-

са EventArgs с описанием события. Второй аргумент мы использовать не

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

ваться. Что касается регистрации обработчиков событий, то для этих це-

лей нами традиционно используются экземпляры делегата EventHandler.

Далее имеет смысл обратиться к программному коду.

Программный код и выполнение

программы

И мы с пути кривого ни разу не свернем,

а надо будет — снова пойдем кривым

путем.

Из к/ф «Айболит 66»

Перед тем как приступить непосредственно к рассмотрению программного

кода, сделаем несколько общих замечаний относительно организации про-

граммы. В частности, есть несколько моментов, на которые имеет смысл

обратить внимание при анализе программы:

 Метка с образцом текста реализуется через объект специального класса, который создается на основе класса метки Label. Мы поступаем следую-

щим образом: на основе класса Label путем наследования создаем класс

MyLabel. Код этого класса состоит, фактически, из конструктора, в кото-

ром определяются основные параметры текстовой метки. У конструк-

тора класса четыре целочисленных аргумента. Аргументы передаются

в метод SetBounds(), который вызывает из объекта метки и определяет

положение и размеры области метки. Также в конструкторе класса зада-

ется тип границ области метки (выделение области рамкой), ее текстовое

значение и способ выравнивания текста в области метки (выравнивание

по центру). Каждый раз, создавая объект класса MyLabel, получаем метку

с соответствующими характеристиками. Вопрос только в том, куда эту

метку добавить. Объект класса MyLabel создается с передачей четырех

целочисленных аргументов.

Программный код и выполнение программы           285

 Для реализации оконной формы создается класс MyForm, который на-

следует класс Form. У класса достаточно много полей, несколько методов

и два свойства. Все основные настройки выполняются в конструкторе

класса.

 Важную роль в рамках использованного в программе подхода играют

текстовые массивы, которые содержат названия кнопок, типы шрифтов, их стили. На основе этих списков формируются массивы объектов для

элементов управления. Это «полуавтоматический» подход, который по-

зволяет достаточно легко добавлять или убирать элемент управления —

во многих случаях достаточно добавить или убрать название элемента

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

проблемы с обработкой событий для добавленных/удаленных элементов

и распределением области оконной формы.

 Диапазон возможных значений размера шрифта определяется мини-

мальным и максимальным значениями, которые реализуются в виде

полей. При создании списка размеров шрифта в одном из пунктов меню

минимальное и максимальное значения размеров шрифта используются

для формирования списка. Сам список формируется специальным ме-

тодом — этот метод в качестве результата возвращает текстовый массив, элементами которого являются числовые значения (их текстовое пред-

ставление) в диапазоне от минимального до максимального значения.

 В качестве полей класса объявляются экземпляры делегата EventHandler (в том числе и один массив из экземпляров делегатов) для обработки

событий, связанных с изменением настроек элементов окна.

 В классе описаны два свойства. Оба только возвращают значения.

Имеется целочисленное свойство для определения размера шрифта, и свойство, которое определяет шрифт, применяемый для образца тек-

ста в окне формы. Это свойство возвращает в качестве значения объект

класса Font. Свойство для определения размера шрифта вычисляется

на основе значения текстового поля. Причем обработка значения поля

выполняется так, что программа не прекращает работу при некоррект-

ном значении поля. Также контролируется «пограничный» режим, в ре-

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

пределы.

 В классе описаны методы, используемые в качестве обработчиков со-

бытий. Эти методы не возвращают результат, и у них по два аргумента: объект класса Object и объект класса EventArgs.

 Для формирования главного меню есть специальный метод. В этом же

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

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

286

Глава 8. Приложение с графическим интерфейсом: учебный проект

неявно предполагается, что делегаты ссылаются на соответствующие

методы. Присваивание значений делегатам выполняется в конструкторе

класса MyForm. Метод для формирования главного меню в конструкторе

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

 Кроме метода для формирования главного меню программы есть метод

для формирования отдельного пункта меню. Этот метод вызывается

в методе для формирования главного меню.

 В некоторых случаях приходится явно преобразовывать числовые зна-

чения в текстовое значение (получать текстовое представление числа).

Для этого из соответствующей числовой переменной вызывается метод

ToString().

Приняв на вооружение все перечисленное выше, можем смело приступить

к «прочтению» программного кода, представленного в листинге 8.1.

Листинг 8.1.  Приложение с графическим интерфейсом

using System;

using System.Drawing;

using System.Windows.Forms;

// Класс для метки с образцом текста:

class MyLabel:Label{

/*

Конструктор класса. Аргументы - координаты левого верхнего

угла области метки и размеры области.

*/

public MyLabel(int x,int y,int w,int h){

Text="Образец текста"; // Текстовое значение метки

SetBounds(x,y,w,h); // Положение и размер метки

BorderStyle=BorderStyle.FixedSingle; // Тип границы области

// метки

// Способ выравнивания текста в метке:

TextAlign=ContentAlignment.MiddleCenter;

}

}

/*

Класс формы. В этом классе описано "практически все".

Класс создается наследованием класса Form.

*/

class MyForm:Form{

// Названия меню:

private string[] MN={"Действие","Тип шрифта","Стиль

шрифта","Размер шрифта"};

// Названия шрифтов:

private string[] FN={"Arial","Times","Courier"};

Программный код и выполнение программы           287

// Стили шрифтов:

private string[] FS={"Жирный","Курсив"};

// Названия кнопок:

private string[] BN={"Применить","Выход"};

// Минимальный размер шрифта:

private int min=10;

// Максимальный размер шрифта:

private int max=20;

// Метод для "вычисления" текстового массива

// целочисленных значений:

private string[] FSz(){

// Текстовый массив нужного размера:

string[] fs=new string[max-min+1];

// Оператор цикла для заполнения текстового

// массива:

for(int i=0;i

fs[i]=(min+i).ToString(); // Преобразование числа в текст

}

return fs; // Результат метода - массив

}

// Метка с образцом текста:

private MyLabel sample;

// Кнопки:

private Button[] Btns;

// Переключатели (для выбора типа шрифта):

private RadioButton[] RBtns;

// Группа переключателей:

private GroupBox FName=new GroupBox();

// Опции (для выбора стиля шрифта):

private CheckBox[] CBtns;

// Текстовое поле для ввода размера текста:

private TextBox tsize;

/*

Группа экземпляров делегатов, используемых

при обработке событий.

*/

private EventHandler[] BH; // Массив экземпляров делегатов

// для кнопок

private EventHandler RBH; // Экземпляр делегата

// для кнопок-переключателей

private EventHandler CBH; // Экземпляр делегата

// для опций

private EventHandler TBH; // Экземпляр делегата

// для текстового поля

// Свойство для определения размера шрифта:

private int FSize{

get{

продолжение

288

Глава 8. Приложение с графическим интерфейсом: учебный проект

Листинг 8.1 (продолжение)

int size; // Локальная целочисленная переменная

try{ // Блок обработки исключительных ситуаций

// Попытка преобразовать текст текстового

// поля в число:

size=Int32.Parse(tsize.Text);

// Если маленькое число, генерируем ошибку:

if(size

if(size>max){ // Если слишком большое число,

// ограничиваем значение

size=max; // Значение локальной переменной

// Присваивание значения текстовому полю:

tsize.Text=size.ToString();

}

return size; // Результат аксессора - значение

// свойства

}

catch{ // Обработка исключительной ситуации

size=min; // Значение локальной переменной - по

// минимуму

tsize.Text=size.ToString(); // Заполнение текстового

// поля

return size; // Значение свойства в случае

// исключительной ситуации

}

}

}

// Свойство для определения шрифта для

// образца текста.

// Свойство является объектом класса Font:

private Font SFont{

get{

FontStyle fs=FontStyle.Regular; // Стиль шрифта. Начальное

// значение

if(CBtns[0].Checked) fs= fs|FontStyle.Bold; // Применяем

// жирный

// шрифт

if(CBtns[1].Checked) fs|=FontStyle.Italic; // Применяем

// курсивный

// шрифт

string fn=FN[0]; // Текстовое название шрифта.

// Начальное значение

// Перебор кнопок-переключателей для

// определения положения переключателя:

for(int i=1;i

if(RBtns[i].Checked) fn=FN[i]; // Изменение названия

// шрифта

Программный код и выполнение программы           289

}

// Создается объект шрифта:

Font F=new Font(fn,FSize,fs);

// Результат свойства:

return F;

}

}

/*

Метод, который используется в качестве обработчика события

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

*/

public void setType(Object obj,EventArgs ea){

string menu; // Локальная текстовая переменная

menu=(obj as MenuItem).Text; // Текст выбранного пункта меню

// Оператор цикла для перебора

// кнопок-переключателей:

for(int i=0;i

if(menu==RBtns[i].Text){

// Если текст пункта меню совпадает

// с текстом кнопки, переключатель

// устанавливается в выделенное положение:

RBtns[i].Checked=true;

return; // Завершается работа метода

}

}

}

/*

Метод, который используется в качестве обработчика события

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

*/

public void setStyle(Object obj,EventArgs ea){

int index; // Локальная целочисленная переменная

index=(obj as MenuItem).Index; // Индекс выбранного пункта

// в меню

// Изменение (инверсия) статуса опции:

CBtns[index].Checked=!CBtns[index].Checked;

}

/*

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

*/

public void setSize(Object obj,EventArgs ea){

string size; // Локальная текстовая переменная

size=(obj as MenuItem).Text; // Текст выбранного пункта меню

tsize.Text=size; // Присваивание нового значения

продолжение

290

Глава 8. Приложение с графическим интерфейсом: учебный проект

Листинг 8.1 (продолжение)

// текстовому полю

}

/*

Метод используется в качестве обработчика события щелчка

на кнопке,

в результате чего применяются настройки шрифта, выполненные

в окне формы.

*/

public void OKButtonClick(Object obj,EventArgs ea){

sample.Font=SFont; // Применение шрифта, определяемого

// свойством SFont

}

/*

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

*/

public void CancelButtonClick(Object obj,EventArgs ea){

Application.Exit(); // Завершение работы программы

}

/*

Метод для создания главного меню. При вызове метода формируется

главное меню оконной формы. Ссылка на объект этого меню

возвращается в качестве

результата.

*/

private MainMenu getMyMenu(){

// Создание объекта главного меню:

MainMenu MyMenu=new MainMenu();

// Создание массива из объектов - пунктов меню:

MenuItem[] mainMI=new MenuItem[MN.Length];

// Оператор цикла для перебора пунктов меню:

for(int i=0;i

mainMI[i]=new MenuItem(MN[i]); // Создание объекта

// пункта меню

// Добавление пункта меню в главное меню:

MyMenu.MenuItems.Add(mainMI[i]);

}

/*

Заполнение командами каждого из пунктов главного меню.

Используется метод setMyMenuItem() для заполнения пунктов меню.

Первый аргумент метода - объект заполняемого пункта меню.

Второй аргумент метода - список текстовых значений-названий команд.

*/

setMyMenuItem(mainMI[0],BN); // Заполнение первого пункта меню

// Регистрация обработчиков событий для выбора

Программный код и выполнение программы           291

// команд первого пункта главного меню:

for(int i=0;i

mainMI[0].MenuItems[i].Click+=BH[i];

}

// Заполнение второго пункта меню:

setMyMenuItem(mainMI[1],FN);

// Регистрация обработчиков событий для выбора

// команд второго пункта главного меню:

for(int i=0;i

mainMI[1].MenuItems[i].Click+=RBH;

}

// Заполнение третьего пункта меню:

setMyMenuItem(mainMI[2],FS);

// Регистрация обработчиков событий для выбора

// команд третьего пункта главного меню:

for(int i=0;i

mainMI[2].MenuItems[i].Click+=CBH;

}

// Заполнение четвертого пункта меню:

setMyMenuItem(mainMI[3],FSz());

// Регистрация обработчиков событий для выбора

// команд четвертого пункта главного меню:

for(int i=0;i

mainMI[3].MenuItems[i].Click+= TBH;

}

// Главное меню сформировано.

// Возвращается результат:

return MyMenu;

}

/*

Метод для формирования пункта меню. Аргументами методами передаются

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

*/

private void setMyMenuItem(MenuItem mm,string[] names){

// Массив объектов класса MenuItem для реализации

// команд пункта меню:

MenuItem[] mi=new MenuItem[names.Length];

// Заполняем пункт меню командами:

for(int i=0;i

mi[i]=new MenuItem(names[i]); // Создание объекта

mm.MenuItems.Add(mi[i]); // Добавление элемента в меню

}

}

// Конструктор класса:

public MyForm(){

продолжение

292

Глава 8. Приложение с графическим интерфейсом: учебный проект

Листинг 8.1 (продолжение)

// Заголовок окна формы:

Text="Работаем со шрифтами";

// Линейные размеры формы:

Height=300;

Width=400;

// Тип границ оконной формы:

FormBorderStyle=FormBorderStyle.FixedSingle;

// Шрифт для элементов оконной формы:

Font=new Font("Arial",8,FontStyle.Bold);

// Создание метки с образцом текста:

sample=new MyLabel(100,140,290,110);

// Добавление метки в окно формы:

Controls.Add(sample);

// Создание массива для объектов кнопок:

Btns=new Button[BN.Length];

// Заполнение массива:

for(int i=0;i

Btns[i]=new Button(); // Создание объекта кнопки

Btns[i].Text=BN[i]; // Название кнопки

Btns[i].SetBounds(10,140+i*40,80,30); // Положение и размеры

// кнопки

Controls.Add(Btns[i]); // Добавление кнопки в окно формы

}

// Массив для кнопок-переключателей:

RBtns=new RadioButton[FN.Length];

// Перебираем элементы массива:

for(int i=0;i

RBtns[i]=new RadioButton(); // Создание объекта

RBtns[i].Text=FN[i]; // Название кнопки-переключателя

RBtns[i].Checked=(i==0); // Состояние переключателя

// Положение и размер кнопки-переключателя:

RBtns[i].SetBounds(10,30+30*i,100,20);

// Добавление кнопки в группу переключателей:

FName.Controls.Add(RBtns[i]);

}

// Название группы переключателей:

FName.Text=MN[1];

// Положение и размер группы переключателей:

FName.SetBounds(10,10,130,120);

// Размещение группы переключателей

// в окне формы:

Controls.Add(FName);

// Текстовая метка "Размер шрифта":

Label lsize=new Label();

// Текстовое значение метки:

lsize.Text=MN[3]+" (от "+min+" до "+max+"):";

Программный код и выполнение программы           293

// Положение и размеры области метки:

lsize.SetBounds(150,20,180,20);

// Добавление текстовой метки в окно формы:

Controls.Add(lsize);

// Создание текстового поле для ввода

// размера текста:

tsize=new TextBox();

// Начальное значение в текстовом поле:

tsize.Text=min.ToString();

// Положение и размеры тестового поля:

tsize.SetBounds(340,20,50,20);

// Способ выравнивания текста в текстовом поле

// (по правому краю):

tsize.TextAlign=HorizontalAlignment.Right;

// Добавление текстового поля в окно формы:

Controls.Add(tsize);

// Массив для кнопок-опций:

CBtns=new CheckBox[FS.Length];

// Перебираем кнопки:

for(int i=0;i

CBtns[i]=new CheckBox(); // Создание объекта опции

CBtns[i].Text="Применить стиль: "+FS[i]; // Текст опции

CBtns[i].Checked=false; // Состояние опции

CBtns[i].SetBounds(150,50+30*i,250,20); // Положение и размер

// опции

Controls.Add(CBtns[i]); // Добавление опции в окно формы

}

/*

Блок с регистрацией обработчиков событий

и сопутствующими командами.

*/

// Массив экземпляров делегатов:

BH=new EventHandler[BN.Length];

BH[0]=OKButtonClick; // Экземпляр делегата для первой

// кнопки

BH[1]=CancelButtonClick; // Экземпляр делегата для второй

// кнопки

// Перебираем кнопки:

for(int i=0;i

Btns[i].Click+=BH[i]; // Регистрация обработчика для

// кнопки

}

// Перебираем кнопки-переключатели:

for(int i=0;i

RBtns[i].CheckedChanged+=BH[0]; // Регистрация обработчика

}

продолжение

294

Глава 8. Приложение с графическим интерфейсом: учебный проект

Листинг 8.1 (продолжение)

// Присваиваем значение экземплярам делегатов:

RBH=setType; // Экземпляр делегата для меню выбора

// типа шрифта

CBH=setStyle; // Экземпляр делегата для меню выбора

// стиля шрифта

TBH=setSize; // Экземпляр делегата для меню выбора

// размера шрифта

// Добавление главного меню в окно формы.

// При вызове метода getMainMenu() используются

// экземпляры делегатов

// для обработчиков событий:

Menu=getMyMenu();

// Применение шрифта к образцу текста:

sample.Font=SFont;

}

}

// Класс с главным методом программы:

class FontApplyDemo {

// Главный метод программы:

public static void Main(){

// Отображаем окно формы:

Application.Run(new MyForm());

}

}

Более детальный анализ некоторых фрагментов этого кода будет приве-

ден несколько позже. Сейчас остановимся на том, как выполняется данная

программа. Так, при запуске программы появляется окно, представленное

на рис. 8.1.

Рис. 8.1.  Вид отображаемого при запуске программы окна формы

Программный код и выполнение программы           295

Как уже отмечалось ранее, окно с названием Работаем со шрифтами содержит

меню из четырех пунктов (Действие, Тип шрифта, Стиль шрифта и Размер шрифта), группу переключателей Тип шрифта на три положения (Arial, Times и Courier), поля с текстом Размер шрифта (от 10 до 20), двух опций (Применить стиль Жир-

ный и Применить стиль Курсив), области образца текста с текстом Образец текста

и двумя кнопками (Применить и Выход).

Пункт меню Действие содержит две команды — Применить и Выход (рис. 8.2).

Рис. 8.2.  Команды пункта меню Действие

Назначение команд такое же, как и одноименных кнопок. В пункте меню

Тип шрифта три команды — Arial, Times и Courier (рис. 8.3).

Рис. 8.3.  Команды пункта меню Тип шрифта

Названия команд не случайно совпадают с названиями переключателей

в группе переключателей Тип шрифта. Выбор команды имеет такой же эф-

фект, как и установка переключателя в одноименное положение.

296

Глава 8. Приложение с графическим интерфейсом: учебный проект

В пункте меню Стиль шрифта всего две команды — Жирный и Курсив (рис. 8.4).

Рис. 8.4.  Команды пункта меню

Стиль шрифта

Выбор команды в пункте меню приводит к установке/отмене флажка соот-

ветствующей опции в области окна формы. В отличие от группы переклю-

чателей, изменение состояния опций к автоматическому изменению пара-

метров шрифта не приводит. Для этого необходимо щелкнуть на кнопке

Применить или выбрать команду Применить в пункте меню Действие. Это же

замечание относится к командам пункта меню Размер шрифта (рис. 8.5).

Рис. 8.5.  Команды пункта меню

Размер шрифта

Список команд пункта меню Размер шрифта — это набор цифр в диапазоне от

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

нию поля ввода соответствующим значением.

Программный код и выполнение программы           297

Несколько следующих рисунков иллюстрируют функциональность окна

формы. Так, на рис. 8.6 показано окно, у которого установлены опции при-

менения жирного стиля и курсива, а в поле размера шрифта указано значе-

ние 18 (настройки выполнены, но не применены).

Рис. 8.6.  Окно с выполненными настройками:

для их применения щелкаем на кнопке Применить

Для применения настроек щелкаем на кнопке Применить. Результат показан

на рис. 8.7.

Рис. 8.7.  Результат применения настроек

Изменение типа шрифта вступает в силу автоматически. На рис. 8.8 по-

казан результат щелчка на переключателе Courier в группе переключателей

Тип шрифта.

298

Глава 8. Приложение с графическим интерфейсом: учебный проект

Рис. 8.8.  При изменении типа шрифта изменения вступают в силу автоматически

В принципе, поскольку поле ввода размера шрифта по своей природе тек-

стовое, в него можно ввести все, что угодно, и не обязательно число. Такие

ситуации обрабатываются корректно — вместо «непонятного» значения

используется размер 10, причем выполняется автоматическая замена зна-

чения в поле ввода. На рис. 8.9 показано окно формы с некорректным зна-

чением в поле размера шрифта.

Рис. 8.9.  Окно перед применением настроек:

в процессе выполнения настроек в поле размера введено некорректное значение

После щелчка на кнопке Применить все корректные настройки вступают

в силу, а в качестве размера шрифта используется значение 10 (рис. 8.10).

Если в поле размера шрифта указать слишком большое (большее 20) зна-

чение, при применении настроек оно «урезается» до 20. На рис. 8.11 в поле

размера шрифта указано значение 10000.

После применения настроек окно выглядит так, как показано на рис. 8.12.

Программный код и выполнение программы           299

Рис. 8.10.  Результат применения настроек с некорректным значением размера шрифта

Рис. 8.11.  Окно перед применением настроек: в поле размера шрифта

введено слишком большое значение

Рис. 8.12.  Результат применения настроек со слишком большим значением

размера шрифта

300

Глава 8. Приложение с графическим интерфейсом: учебный проект

Интересно в данном случае то, что размер шрифта стал равен 20. Анало-

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

слишком маленькое значение (меньшее 10). Разница в этом случае лишь

такая, что применяется не «максимальный» шрифт 20, а «минимальный»

шрифт 10.

Наиболее значимые места

программного кода

Я стану этим... Вот этим... Нет, этим я не

смогу. Впрочем, я стану другом короля!

Из к/ф «Дон Сезар де Базан»

В качестве финального штриха обсудим некоторые блоки или фрагменты

кода, которые позволяют «зафиксировать» основные и «тонкие» места ис-

пользованного нами алгоритма.

Класс MyLabel нами уже упоминался. Объектная ссылка sample этого клас-

са объявлена полем класса MyForm. Создание объекта класса выполняется

в конструкторе класса MyForm командой sample=new MyLabel(100,140,290, 110). То есть область этой текстовой метки в окне формы имеют фиксиро-

ванное положение и размер. Добавление метки в окно формы выполняется

командой Controls.Add(sample).

Здесь  проиллюстрирован  один  достаточно  продуктивный  подход, который состоит в том, что для графических элементов с определен-

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

класс. Мы один раз в классе описываем характеристики и параметры

элемента,  а  потом  для  создания  элемента  соответствующего  типа

и вида создаем объект данного класса. Хотя в нашем примере это не

очень заметно, но на практике это очень удобно.

Объект sample используется в методе OKButtonClick(). Метод содержит ко-

манду sample.Font=SFont, которой свойству Font объекта sample в качестве

значения присваивается значение свойства SFont. Эта же команда встре-

чается в конструкторе класса MyForm (последняя команда). В конструк-

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

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

Метод OKButtonClick() является обработчиком события щелчка на кноп-

ке Применить. Что касается свойства SFont, значение свойства формируется

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

Наиболее значимые места программного кода           301

когда запрашивается это свойство (а это происходит при выполнении ме-

тода OKButtonClick()), автоматически «считываются» настройки элемен-

тов в окне формы и на их основе вычисляется нужный шрифт (создается

объект шрифта). Что касается шрифта, применяемого в оконной форме, то

он определяется командой Font=new Font("Arial",8,FontStyle.Bold), ко-

торой свойству Font формы присваивается объект шрифта, создаваемый

командой new Font("Arial",8,FontStyle.Bold). В данном случае речь идет

о жирном шрифте типа Arial размера 8.

Текстовые массивы MN, FN, FS и BN определяют, соответственно, названия

пунктов главного меню, названия шрифтов, названия стилей шрифтов

и названия кнопок. Эти массивы играют важную роль. Дело в том, что та-

кие объекты, как кнопки Btns, радиокнопки (кнопки-переключатели) RBtns и опции CBtns, реализуются в виде массивов объектов (объектных пере-

менных). Более того, внутренние команды пунктов главного меню также

реализуются как массивы. И все соответствующие вычисления (в первую

очередь те, что касаются количества элементов) выполняются на основе

«базовых» текстовых массивов.

ПРИМЕЧАНИЕ Несколько особо обстоят дела с текстовым массивом из «чисел».

Массив возвращается как результат методом FSz(). В теле метода на

основе значений целочисленных полей min и max создается тексто-

вый массив размера max-min+1. Затем массив заполняется числами, преобразованными в текст, и возвращается в качестве результата.

Поэтому, если нам нужен массив из текстовых представлений чисел

в диапазоне от min до max, мы используем в качестве ссылки на такой

массив инструкцию FSz().

В основном все эти действа происходят в конструкторе класса MyForm.

Например, массив кнопок (массив объектных переменных) создается

командой Btns=new Button[BN.Length]. Здесь размер массива кнопок со-

впадает с размером массива названий кнопок, что вполне логично. Затем

в операторе цикла индексная переменная i перебирает элементы кнопоч-

ного массива, и за каждый цикл выполняется создание объекта (коман-

да Btns[i]=new Button()), присваивание имени кнопке в соответствии

с текстовым значением «базового» текстового массива (команда Btns[i].

Text=BN[i]), определение позиции и размеров кнопки (команда Btns[i].

SetBounds(10,140+i*40,80,30)) и добавление кнопки в окно формы (ко-

манда Controls.Add(Btns[i])). Похожим образом все происходит и для

кнопок-переключателей RBtns и опций CBtns, с поправкой на имя «базо-

вого» текстового массива. Правда, у этих элементов задается еще свойство

Checked, которое отвечает за состояние элемента (выделен или нет). Для оп-

ций значение этого свойства устанавливается равным false (в начальный

302

Глава 8. Приложение с графическим интерфейсом: учебный проект

момент опции не выделены), а для кнопок-переключателей значение свой-

ства задается равным (i==0), в силу чего выделенным будет первый пере-

ключатель (для которого индекс i равен нулю).

Кнопки-переключатели необходимо объединить в группу, а уже потом

группа переключателей добавляется в форму. Отдельные переклю-

чатели добавляются не непосредственно в форму, а в группу пере-

ключателей. В программе есть объект FName класса GroupBox. Метод

Add() для отдельных кнопок-переключателей вызывается из объекта

FName. А для добавления в форму группы, метод Add() с аргументом

FName вызывается из объекта формы.

Мы намеренно разнесли во времени и пространстве процесс создания гра-

фических элементов и регистрацию обработчиков для элементов интер-

фейса. В программе используются экземпляры делегата EventHandler BH

(массив из экземпляров делегата для регистрации обработчиков щелчка

на кнопках в области формы и команд первого пункта главного меню, ко-

торые ссылаются на методы OKButtonClick() и CancelButtonClick()), RBH

(экземпляр делегата для обработки выбора команд второго пункта меню со

ссылкой на метод setType()), CBH (экземпляр делегата для обработки выбо-

ра команд третьего пункта меню со ссылкой на метод setStyle()) и TBH (эк-

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

ссылкой на метод setSize()). Для кнопок экземпляры делегата регистри-

руются для события Click (происходит при щелчке на кнопке). Экземпляр

делегата BH[0] регистрируется также для события CheckedChanged кнопок-

переключателей (происходит при изменении статуса переключателя). По-

скольку экземпляр делегата BH[0] регистрируется для кнопки Применить, изменение положения переключателей приводит к выполнению того же

метода, что и щелчок на кнопке Применить. Прочие экземпляры делегата ис-

пользуются при создании главного меню. И это отдельная история.

Кульминацией процесса создания главного меню является команда

Menu=getMyMenu() в конструкторе класса MyForm. Командой свойству Menu присваивается результат метода getMyMenu(). Несложно догадаться, что

именно этим методом создается и возвращается в качестве результата

главное меню формы.

Метод в качестве результата возвращает объект класса MainMenu. В теле

метода создается объект MyMenu класса MainMenu и массив mainMI объектов

класса MenuItem. Это пункты главного меню. Каждый новый пункт главно-

го меню добавляется методом Add() в коллекцию MenuItems объекта MyMenu.

Метод Add() вызывается из коллекции MenuItems, которая является полем

объекта MyMenu. Аргументом методу Add() передается добавляемый пункт

меню (объект, соответствующий этому пункту).

Наиболее значимые места программного кода           303

Заполнение командами каждого из пунктов главного меню выполняется

с помощью метода setMyMenuItem(). Аргументами методу передаются объ-

ект заполняемого пункта меню и список команд пункта меню (в виде тек-

стового массива).

ПРИМЕЧАНИЕ Метод setMyMenuItem() не возвращает результат. В теле метода созда-

ется массив mi объектов класса MenuItem. Аргументом конструктору

класса  MenuItem  передаются  текстовые  названия  команд.  Добав-

ление команды меню в пункт меню выполняется через коллекцию

MenuItems с помощью метода Add().

Кроме этого, для команд разных пунктов меню выполняется регистрация

обработчиков событий. Здесь есть два важных момента. Во-первых, ко-

манда вида mainMI[k].MenuItems[m] означает m+1-ю команду в k+1-м пункте

главного меню, а событие Click для команды меню означает выбор пользо-

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

ного, для всех команд пункта меню в качестве обработчика регистрируется

один и тот же метод. Поэтому такой метод должен уметь как-то различать

разные команды в пределах пункта меню. В каждом методе-обработчике

эта задача решается по-разному.

Метод setType() вызывается для обработки выбора пункта меню, свя-

занного с определением типа шрифта. В теле метода объект obj (аргу-

мент), вызвавший событие, командой obj as MenuItem приводится к типу

MenuItem и для этого объекта считывается свойство Text (название ко-

манды). Затем с помощью оператора цикла ищется совпадение названия

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

реключателя происходит событие CheckedChanged, а на этот случай уже

имеется обработчик.

Метод setStyle() используется в качестве обработчика события выбора

пункта меню, связанного с определением стиля шрифта. В этом случае

определяется индекс команды пункта меню (свойство Index) и для опции

в окне формы с таким же индексом статус меняется на противополож-

ный — выделенная опция становится невыделенной, и наоборот.

Метод setSize() используется для обработки события выбора пункта

меню, связанного с определением размера шрифта. Здесь мы считываем

название команды (свойство Text) и присваиваем его в качестве значения

(свойство Text) текстовому полю (объект tsize-поле класса MyForm).

Важную роль в программном коде играет свойство SFont. У свойства име-

ется только get-аксессор, в котором на основе положения переключателей

типа шрифта, состояния опций стиля шрифта и значения текстового поля

304

Глава 8. Приложение с графическим интерфейсом: учебный проект

с размером шрифта формируется объект класса Font, который и возвраща-

ется в качестве результата (значения свойства). При этом размер шрифта

не просто считывается из текстового поля, но и обрабатывается. Для этого

в программе предусмотрено свойство FSize. В единственном get-аксессоре

этого свойства выполняется попытка преобразовать число в текст. За счет

try­catch блока, если такая попытка неудачна, в качестве значения разме-

ра шрифта используется минимально допустимое. Также отслеживаются

случаи выхода значения размера шрифта за допустимые пределы. В случае

если размер шрифта меньше минимально допустимого, искусственно ге-

нерируется ошибка, которая перехватывается блоком try­catch. Слишком

большие числовые значения отлавливаются с помощью условного опера-

тора. В любом случае применяемый шрифт, если он не совпадает с перво-

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

ВМЕСТО ЗАКЛЮЧЕНИЯ Графический

конструктор

Пока это лекция. И даже скучная лекция.

Из к/ф «В поисках капитана Гранта»

В книгах Вступление и Заключение играют очень важную роль. Во Всту-

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

тателю нужна больше всего и именно из этой книги читатель почерпнет

столько знаний, что прочие книги ему уже и не понадобятся. В Заключе-

нии обычно дается краткое пояснение по поводу того, почему чуда не слу-

чилось. Короче говоря, без Вступления и Заключения не обойтись никак.

Мы постараемся отойти от канонов и употребить Заключение во благо, а не

в наущение. Но мистическую связь Вступления и Заключения разрывать

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

фического конструктора, встроенного в среду Visual C# Express, прибегать

не будем. Здесь мы очень кратко покажем, как в графическом редакторе

можно создать простенькое функциональное окно. Ну а пытливый чита-

тель легко сможет экстраполировать подход и для создания более сложных

приложений.

306

Вместо заключения. Графический конструктор

Создание простого окна с кнопкой

— Ну что, не передумали?

— Мне выбирать не приходится.

Из к/ф «Приключения Шерлока Холмса

и доктора Ватсона. Знакомство»

В качестве иллюстрации возможностей среды разработки Visual C# Express 2010 рассмотрим процесс создания простенького приложения с очень

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

Итак, запускаем среду разработки Visual C# Express 2010. Для созда-

ния нового проекта используем команду Создать  новый  проект из меню

Файл. В качестве типа приложения указываем Приложение  Windows  Forms (рис. З.1).

Рис. З.1.  Создаем Windows-приложение

По умолчанию новое приложение содержит окно формы (рис. З.2).

Если это графическое окно выделить (выбрать) мышкой, можно путем

перетаскивания границ изменить размеры этого окна по желанию пользо-

вателя. Так же легко выполняются и прочие настройки окна формы. Для

Создание простого окна с кнопкой           307

этого нам понадобится окно свойств. Отобразить окно можно с помощью

команды ВидДругие окнаОкно свойств. Например, на рис. З.3 показано, как

устанавливается значение Text для окна формы.

Рис. З.2.  Изменяем размеры оконной формы

Рис. З.3.  Свойство Text определяет заголовок окна формы

308

Вместо заключения. Графический конструктор

Это свойство определяет заголовок окна. В окне свойств содержится также

набор из огромного количества свойств оконной формы, которые опреде-

ляют ее вид и функциональность.

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

функциональные компоненты. Для этого на панели элементов выбирает-

ся пиктограмма добавляемого элемента, и затем мышкой в области формы

выделяется область, куда будет помещен элемент.

Панель элементов можно открыть с помощью команды ВидДругие

окнаПанель элементов.

На рис. З.4 на панели элементов выбирается элемент Label, что соответству-

ет текстовой метке.

Рис. З.4.  Выбираем объект Label для добавления в окно формы

Иллюстрация процесса размещения текстовой метки в области окна фор-

мы представлена на рис. З.5.

По умолчанию кнопка имеет банальное содержимое, которое имеет смысл

заменить. За содержимое текстовой метки ответственно свойство Text.

В окне свойств задаем значение этого свойства для метки, как показано на

рис. З.6.

Создание простого окна с кнопкой           309

Рис. З.5.  Размещение в окне формы текстовой метки

Рис. З.6.  Свойство Text текстовой метки определяет ее содержимое

У  каждого  компонента  свой  набор  свойств.  Поэтому,  изменяя  на-

стройки того или иного компонента, следует следить за тем, чтобы на-

стройки выполнялись в окне свойств именно для этого компонента.

Свойство Font определяет параметры шрифта, который применяется для

отображения содержимого текстовой метки. На рис. З.7 показано, как на-

страивается шрифт текстовой метки.

310

Вместо заключения. Графический конструктор

Рис. З.7.  Задаем свойства метки (текст и шрифт)

Как мы и обещали, в оконную форму добавляем кнопку. Для этого в окне

панели элементов необходимо выбрать элемент Button (рис. З.8).

Рис. З.8.  Выбираем для вставки в форму объект кнопки Button

Создание простого окна с кнопкой           311

Процесс размещения кнопки в окне формы показан на рис. З.9.

Рис. З.9.  Добавление кнопки в окно формы

Как и в случае текстовой метки, свойства кнопки придется настраивать.

Для кнопки мы задаем свойство Text (название кнопки) и свойство Font (шрифт для отображения названия). Эти свойства настраиваются, как не-

сложно догадаться, в окне свойств, открытом для кнопочного компонента

(рис. З.10).

Рис. З.10.  Настройка параметров кнопки (текст кнопки и шрифт) На этом настройка внешнего вида формы закончена. Теперь еще необхо-

димо «научить» кнопку реагировать на щелчок. Для этого в режиме гра-

фического конструктора выполняем мышкой двойной щелчок на кнопке.

312

Вместо заключения. Графический конструктор

В результате мы автоматически окажемся переброшенными к программ-

ному коду обработчика щелчка на кнопке. Там вся оболочка уже есть, и нам

предстоит добавить лишь непосредственно те команды, которые должны

выполняться при щелчке на кнопке. Мы хотим, чтобы приложение в этом

случае завершало работу. Поэтому вводим уже знакомую нам команду, представленную в листинге З.1.

Листинг З.1.  Команда, вводимая в обработчик щелчка на кнопке

Application.Exit();

То место, куда вводится эта команда, показано и специально выделено

в документе на рис. З.11.

Рис. З.11.  Добавляем программный код для обработки щелчка на кнопке

В принципе, еще нужен программный код, который будет отображать

оконную форму при запуске приложения. Но этот код генерируется авто-

матически. Увидеть его можно, выполнив двойной щелчок на пиктограмме

Program.cs в окне Обозреватель решений (рис. З.12).

Самая главная команда этого кода выделена. Нам она тоже знакома (см. ли-

стинг З.2).

Листинг З.2.  Команда, которой отображается форма (предлагается по умолчанию) Application.Run(new Form1());

Создание простого окна с кнопкой           313

Рис. З.12.  Здесь ничего добавлять не нужно — все добавлено без нас

Собственно, приложение готово к использованию. При запуске приложе-

ния открывается окно, представленное на рис. З.13.

Рис. З.13.  При щелчке на кнопке Закрыть окно закрывается

Если в этом окне щелкнуть на кнопке Закрыть, окно закроется. По тому

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

сводится к размещению в окне формы нужных элементов, настройке их

свойств и составлению программного кода обработчиков событий.

Алфавитный указатель

А

главный, 32, 34, 58

Аксессор, 176, 184

обобщенный, 256

операторный, 144

Д

перегрузка, 35, 41, 60

переопределение, 35, 85, 90, 164,

Делегат, 32, 175, 193

172, 219

Деструктор, 64, 70

сигнатура, 60

статический, 32

З

Замещение членов, 85, 90

Н

Наследование, 72

И

многоуровневое, 83

Индексатор, 32, 175, 184

Небезопасный код, 141

Инструкция безусловного перехода,

115

О

Интерфейс, 85, 211, 227

Объект, 31, 56

Интерфейсная переменная, 85, 238

анонимный, 192, 197

Исключительная ситуация, 47, 50,

Объектная переменная, 56, 57, 81,

116, 265

85, 134, 214, 238, 245

ООП, 8, 34, 193

К

Оператор

Класс, 30, 54

выбора, 110

абстрактный, 211, 218

перегрузка, 108, 143, 163

базовый, 73, 81

приведения типа, 157, 168, 173

обобщенный, 256, 259

присваивания, 101, 102, 107

оболочка, 99

тернарный, 101, 107

производный, 73, 81

условный, 48, 96, 107, 108, 116

Комментарий, 30

цикла, 47, 112, 113, 114, 116, 133

Константа, 41

Конструктор, 56, 64, 72, 168

П

базового класса, 77

Переменная массива, 126, 134

создания копии, 68

Перечисление, 41, 96, 211

статический, 97

Поле, 32

Поток, 273

М

Пространство имен, 33

Массив, 125

Метод, 32

Р

абстрактный, 218

Рекурсия, 96

виртуальный, 90

Алфавитный указатель           315

С

Свойство, 32, 175

Событие, 32, 175, 199, 203

Статический член, 93

Структура, 211, 214

У

Указатель, 140

Ц

Цикл, 47

Алексей Николаевич Васильев

C#. Объектно-ориентированное программирование: Учебный курс

Заведующий редакцией

А . Кривцов

Руководитель проекта

А . Юрченко

Ведущий редактор

Ю . Сергиенко

Литературный редактор

О . Некруткина

Художественный редактор

К . Радзевич

Корректор

И . Тимофеева

Верстка

Л . Волошина

ООО «Мир книг», 198206, Санкт-Петербург, Петергофское шоссе, 73, лит. А29.

Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 95 3005 — литература учебная.

Подписано в печать 05.03.12. Формат 70х100/16. Усл. п. л. 25,800. Тираж 2000. Заказ 0000.

Отпечатано по технологии CtP в ОАО «Первая Образцовая типография», обособленное подразделение «Печатный двор».

197110, Санкт-Петербург, Чкаловский пр., 15.

Document Outline

Вместо вступления. Язык программирования C#

Краткий курс истории языкознания

Особенности и идеология C#

Программное обеспечение

Установка Visual C# Express

Немного о книге

Благодарности

От издательства

Глава 1. Информация к размышлению: язык C# и даже больше

Очень простая программа

Несколько слов об ООП

Еще одна простая программа

Консольная программа

Глава 2. Классы и объекты

Описание класса

Объектные переменные и создание объектов

Перегрузка методов

Конструкторы и деструкторы

Наследование и уровни доступа

Объектные переменные и наследование

Замещение членов класса и переопределение методов

Статические члены класса

Глава 3. Основы синтаксиса языка C#

Базовые типы данных и основные операторы

Основные управляющие инструкции

Массивы большие и маленькие

Массивы экзотические и не очень

Знакомство с указателями

Глава 4. Перегрузка операторов

Операторные методы и перегрузка операторов

Перегрузка арифметических операторов и операторов приведения типа

Перегрузка операторов отношений

Глава 5. Свойства, индексаторы и прочая экзотика

Свойства

Индексаторы

Делегаты

Знакомство с событиями

Элементарная обработка событий

Глава 6. Важные конструкции

Перечисления

Знакомство со структурами

Абстрактные классы

Интерфейсы

Интерфейсные переменные

Глава 7. Методы и классы во всей красе

Механизм передачи аргументов методам

Аргументы без значений и переменное количество аргументов

Передача типа в качестве параметра

Использование обобщенного типа данных

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

Многопоточное программирование

Глава 8. Приложение с графическим интерфейсом: учебный проект

Общие сведения о графических элементах

Программный код и выполнение программы

Наиболее значимые места программного кода

Вместо заключения. Графический конструктор

Создание простого окна с кнопкой

Перейти на страницу:

Похожие книги

1С: Бухгалтерия 8 с нуля
1С: Бухгалтерия 8 с нуля

Книга содержит полное описание приемов и методов работы с программой 1С:Бухгалтерия 8. Рассматривается автоматизация всех основных участков бухгалтерии: учет наличных и безналичных денежных средств, основных средств и НМА, прихода и расхода товарно-материальных ценностей, зарплаты, производства. Описано, как вводить исходные данные, заполнять справочники и каталоги, работать с первичными документами, проводить их по учету, формировать разнообразные отчеты, выводить данные на печать, настраивать программу и использовать ее сервисные функции. Каждый урок содержит подробное описание рассматриваемой темы с детальным разбором и иллюстрированием всех этапов.Для широкого круга пользователей.

Алексей Анатольевич Гладкий

Программирование, программы, базы данных / Программное обеспечение / Бухучет и аудит / Финансы и бизнес / Книги по IT / Словари и Энциклопедии
1С: Управление торговлей 8.2
1С: Управление торговлей 8.2

Современные торговые предприятия предлагают своим клиентам широчайший ассортимент товаров, который исчисляется тысячами и десятками тысяч наименований. Причем многие позиции могут реализовываться на разных условиях: предоплата, отсрочка платежи, скидка, наценка, объем партии, и т.д. Клиенты зачастую делятся на категории – VIP-клиент, обычный клиент, постоянный клиент, мелкооптовый клиент, и т.д. Товарные позиции могут комплектоваться и разукомплектовываться, многие товары подлежат обязательной сертификации и гигиеническим исследованиям, некондиционные позиции необходимо списывать, на складах периодически должна проводиться инвентаризация, каждая компания должна иметь свою маркетинговую политику и т.д., вообщем – современное торговое предприятие представляет живой организм, находящийся в постоянном движении.Очевидно, что вся эта кипучая деятельность требует автоматизации. Для решения этой задачи существуют специальные программные средства, и в этой книге мы познакомим вам с самым популярным продуктом, предназначенным для автоматизации деятельности торгового предприятия – «1С Управление торговлей», которое реализовано на новейшей технологической платформе версии 1С 8.2.

Алексей Анатольевич Гладкий

Финансы / Программирование, программы, базы данных