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

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

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

Переопределение не следует путать с перегрузкой. При перегрузке

создается  метод  с  таким  же  именем,  но  с  другой  сигнатурой.  При

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

совпадают. Фактически, переопределение метода означает, что он соз-

дается заново. Переопределение выполняется с атрибутом 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++) такая ситуа-

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

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

С другой стороны, множественное наследование — очень мощный

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

Загрузка...