Опубликован: 06.10.2011 | Доступ: свободный | Студентов: 1678 / 90 | Оценка: 4.67 / 3.67 | Длительность: 18:18:00
Дополнительный материал 2:

Введение в C# (по материалам Бенджамина Моранди)

Деструкторы

В C# предполагается существование сборщика мусора: в то время как создание объектов выполняется явно при задании операции new, освобождение памяти для неиспользуемых более объектов возлагается на автоматический механизм - сборщик мусора (Garbage Collector - GC). Иногда можно попросить GC выполнить специфическую операцию, помимо освобождения памяти. Типичным примером является работа с файлами. Всякий раз, когда закончена работа с объектом, представляющим файл, требуется закрыть физический файл, связанный с объектом (в Eiffel можно для этих целей вызывать процедуру dispose, которую GC будет выполнять при освобождении объекта).

Деструкторы C# отвечают этим потребностям. Имя деструктора ~C, где C - имя класса. Здесь нет перегрузки, так как деструктор существует, если он задан, в единственном экземпляре. Он не имеет аргументов, не возвращает значения, не имеет модификаторов, таких как static:

class File {
   … Другие компоненты, включая конструкторы …
   ~File() {
      … Операторы, закрывающие, например, физический файл … 
} 
}
Операции

Операция, определяемая программистом, - это статический метод, где знак операции выступает в качестве имени метода, как в примере:

class E {
    public static E operator +(E a, E b) {
      … Вычисление результата exp, возвращаемого в операторе return …
      return exp;
      }
}

Операция может быть вызвана в инфиксном синтаксисе, свойственном операциям в математике, как x + y, где x и y типа E (это аналогично alias-компоненту Eiffel). Операции встроены в базисные типы, такие как int и float. Имена (знаки) операций и их приоритеты фиксированы (в отличие от Eiffel, нельзя определять собственные знаки операций). Главными доступными знаками операций являются:

+    -   ~   !	 ++

Здесь "!" - отрицание для булевских, "~" - отрицание для целых (взаимное обращение 0 и 1 в бинарном представлении числа). Операции "++" и " " имеют побочный эффект: x++ возвращает значение x, затем увеличивает x на 1; ++x также увеличивает x, возвращая увеличенное значение; (аналогичная семантика у операций x- и ). Как вы знаете, операции с побочным эффектом - не очень хорошая идея, используйте их на собственный страх и риск.

Бинарными операциями являются:

+ - _ / % ^	                    //	Арифметика (% - остаток от деления нацело)
<  > <= >=	        //	Операции отношения (дают булевский результат)
== !=	                        //	Эквивалентность (равно и не равно)
& ^ |	                    //	Булевские строгие (and, xor, or)
<<  >>	            //	Побитовый сдвиг (влево, вправо)
&&  ||	                //	Булевские полустрогие (and, or)

Как и в других языках, наследуемых от C, знак равенства означает присваивание, а эквивалентность задается двумя знаками равенства. Некоторые из приведенных выше операций применяются к побитовому представлению целых. Строгие булевские операции применимы не только к булевским значениям, но и к целым, применяя операцию к каждой паре соответствующих битов. Операции сдвига сдвигают битовое представление влево и вправо на число позиций, задаваемое вторым операндом, при этом биты, выходящие за края сетки, отведенной целому, исчезают, а свободные места заполняются нулями. Сдвиг влево m << n эквивалентен умножению m на 2^n, сдвиг вправо - делению.

Можно использовать перечисленные знаки для определения операций в собственных классах, исключением являются знаки полустрогих операций. Механизм перегрузки используется и для операций. В случае операций сравнения перегрузка должна идти парами - если перегружается операция "меньше" (<), то необходимо определить и "больше" (> ). Аналогично и для других операций сравнения.

Кроме того, C# поддерживает следующие неперегружаемые операции:

[…]	                                                          //	Получение элемента массива
(…)	                                                          //	Кастинг
+=	-=   _=   /=   %=   ^=                                    //	Присваивание
&=	|=   <<=     >>=                          //	Присваивание

Присваивание x += 1 это сокращенная форма записи x = x + 1. Аналогичный смысл и у других операций присваивания с операциями.

Массивы и индексаторы

Для объявления массива с одной или более размерностью используется нотация с квадратными скобками:

string[] a;	                                              // Одномерный массив
string[,] b;                                              // Двумерный массив

Для задания многомерных массивов используются запятые, как показано в примере. Число запятых, увеличенное на 1, определяет размерность массива. Нижняя граница индекса по каждому измерению фиксирована и равна 0. Поэтому элемент b [1, 1] задает элемент во второй строке и втором столбце. Такая политика требует внимательности при работе с индексами.

В объявление типа массива границы не входят. Массивам память отводится динамически (как в Eiffel). Присваивание:

a = new string[4];

создаст массив из четырех элементов, инициализируемых стандартными значениями по умолчанию. Разрешается инициализировать массив значениями в момент его создания:

a = new string [] {"A", "B", "C", "D"};    //Массив из четырех элементов

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

b = new string[2, 3];
b = new string[,] {{"A", "B"}, {"C", "D"}, {"E", "F"}}; // Размерность [3,2]

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

string[][] c;

Каждая строка может иметь различный размер (отсюда и название - изрезанность, гребенка). Вот пример типичной инициализации:

c = new string[][] {new string[] {"A"},
                         new string[] {"B", "C", "D"}, 
                         new string[] {"E", "F"}};

Число элементов в каждой строке соответственно - 1, 3, 2.

Доступ и модификация использует синтаксис с квадратными скобками:

b[0, 0] = "Z";
c [0] = new string[] {"Y", "Z"};   // Изменяет первую строку с индексом 0
c [0][0] = "Z";

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

Table t = new Table (); string n;
…
n = t [1, 1];  // Доступ к первому элементу (смотри реализацию ниже)

Здесь Table - собственный класс, поставляемый с индексатором, выполняющим ту же роль, что и псевдоним (alias) " []" - "квадратные скобки" в Eiffel. Определение индексатора является обобщением метода-свойства:

class Table {
   private string[ , ] rep;// Инициализация rep опущена 
   public string this [int i, int j] { 
     get {return rep [i - 1, j - 1];} 
     set {rep [i - 1, j - 1] = value;} 
   } 
}

Имя индексатора фиксировано и совпадает с именем текущего объекта this, как следствие, у класса может быть только один индексатор. Индексатор определяет два метода - геттер и сеттер с аргументами, задающими индексы элемента контейнера, к которому осуществляется доступ. В реализации класса Table индексация идет по двумерному массиву rep и устроена так, что для клиента начальный индекс по обоим измерениям начинается с единицы.

Универсальность

Концепция универсальности C# знакома по Eiffel; родовые параметры заключаются в угловые скобки <…>. Объявим:

class F<G, H> where H: T, new() {
    … Объявление класса … 
}

Класс F имеет два родовых параметра. Для H задано ограничение (как в классе C [G, H -> T] в Eiffel), так что любой фактический родовой параметр должен наследовать от T. Включение в ограничение new() означает (как в Eiffel, если объявление имеет вид C [G, H -> T create make end] для процедуры создания make из T), что T должен обеспечить public-кон-структор без аргументов; это позволит методам F создавать экземпляры T.

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

F<V, W>  //W должен быть согласован с T и иметь конструктор без аргументов.
Основные операторы

Дадим обзор основных операторов языка.

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

var = e;

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

Вызов метода использует имя метода и список аргументов. В отличие от вызова в Eiffel, круглые скобки всегда сопровождают вызов метода, даже если список аргументов пуст, как в methodWithNoArgument ().

Оператор return не имеет прямого аналога в Eiffel. Он обязателен для функций, задавая значение, возвращаемое функцией:

return some_expression;

Для процедур оператор возможен и задает завершение процедуры. Поскольку в методах он может появляться в нескольких местах, это означает, что методы C# не соответствуют правилу "один выход".

Управляющие структуры

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

if (c1) {
   … 
} else if (c2) {
   … 
} else {
   … 
}

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

Множественный выбор имеет форму:

switch (expression) {
   case value: statement; break;
   case value: statement; break;
   …
   default: statement; break 
}

Здесь expression задается булевским или целочисленным выражением, а каждое value представляет вычислимую в период компиляции константу. Когда значение выражения не совпадает ни с одной константой, выполняется ветвь default, если она задана, в противном случае ничего не делается (в Eiffel в отсутствие ветви else в операторе inspect в подобной ситуации в период выполнения генерируется ошибка). Оператор switch не задает конструкцию с одним входом и одним выходом, а представляет многоцелевой goto. Для правильной структурированности следует четко следовать показанной схеме. Принудительный оператор break, завершающий каждую ветвь, позволяет избежать типичной ошибки для C++ и C-версии switch, когда управление проваливается в другую ветвь.

Доступно несколько форм оператора цикла. Наиболее общая идет от С и соотносится с конструкцией from в Eiffel:

for (initialization; exit; modification) {
… body …
}

Вначале выполняется инициализация цикла - initialization - и работа цикла заканчивается, если условие выхода из цикла - exit - получает в результате значение true. Обычно оно не выполняется после инициализации, и тогда выполняется тело цикла, а затем модификация параметров цикла - modification, после чего снова проверяется условие выхода. Цикл завершается, когда выполняется условие выхода. Модификация обеспечивает продвижение к следующему шагу, увеличивая индекс, продвигая курсор (в Eiffel она интегрирована с телом цикла).

Можно использовать циклы в стиле while или until:

while (condition) {statements} 
do {statements} while (condition)

Удивительно, C# сохранил оператор goto M, где M - метка. Операторы можно снабжать метками, отделяя метку от оператора двоеточием.

Обработка исключений

Исключение - это событие периода выполнения программы, появление которого приводит к прерыванию нормального выполнения программы. Причины исключений могут быть разные, такие как деление целого на нуль, отсутствие файла, null-ссылка у цели, вызывающей метод. Возможно также, что исключение "выбрасывается" при выполнении специального оператора языка C#:

throw e;

Здесь e - тип исключения, который должен быть потомком библиотечного класса Exception.

Обработка исключений в C# выполняется в следующем стиле:

try {
   … Обычные операторы, во время выполнения которых может возникнуть
   … исключение … 
} catch (ET1 e) {
   … Обработка исключения типа ET1, детали в объекте e.… 
} catch (ET2 e) {
   Обработка исключения типа ET2, детали в объекте e.… 
}… Возможно, другие случаи … 
finally {
    … Выполняется во всех случаях, было исключение или нет. 
}

Если в охраняемом try-блоке включается исключение одного из перечисленных типов ET1, ET2, …, то выполнение в try-блоке прерывается и управление передается соответствующему catch-блоку, который в состоянии захватить исключение данного типа. Блок finally выполняется всегда, если присутствует. Его обычная цель - освобождение ресурсов, закрытие файлов и так далее.

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

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

Читатель, знакомый с Java, заметил, что приведенное описание применимо к обоим языкам (получившим этот механизм от С++). Есть отличие от модели Java - метод в C# не специфицирует типы выбрасываемых исключений, как это делает Java, используя спецификацию throws.

Делегаты и события

В C# предлагается механизм делегатов (аналог агентов Eiffel) для описания методов как объектов. Ассоциированный механизм - события - дополняет делегаты для программирования, управляемого событиями (в Eiffel типы событий описываются как обычные объекты и, следовательно, нет необходимости в специальных конструкциях). Рассмотрим объявление делегата:

public delegate int DT (string s);
4.

Это объявление типа: оно определяет тип D T, который представляет функции с одним строковым аргументом, возвращающие целое в качестве результата. Для определения экземпляра класса и связывания его с конкретным методом - скажем, int lettercount(string s) - функцией, подсчитывающей число буквенных символов, - можно использовать:

DT d = new DT(lettercount)
5.

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

DT d = lettercount;	
6.

Некоторые языки программирования (отличные от функциональных) не позволяют использовать метод как аргумент другого метода. Механизмы агентов, делегатов, указателей функций в С++ спроектированы как раз с целью создания специальных объектов, передаваемых как аргументы и представляющих обертки соответствующих функций. Для создания в C# делегата из метода необходимо передать метод конструктору, как в [5]. Концептуально это единственный случай, допускающий использование имени метода как значения.

На практике C# ослабляет правило, допуская присваивания, такие как [6], или передавая имя метода как аргумент другому методу, но это синтаксический сахар, фактически передаваемое значение является делегатом, здесь new DT ( ) .

Делегат можно вызывать подобно любому другому методу:

n = d ("A");	
7.

После присваивания [5] или [6] эффект будет тот же, как и при непосредственном вызове n = lettercount ("A") ;. Конечно, при вызове [7] обычно неизвестно, какую именно функцию представляет d. Часто такой вызов осуществляется в методе r, для которого d - формальный аргумент, и вместо присваивания, такого как [6], передается делегат в качестве фактического аргумента:

r (lettercount);

Эквивалент Eiffel комбинации [4] или [5] задается одним оператором d:= agent lettercount, не требующим объявления типа, такого как [4]. Эта форма не имеет прямого эквивалента в C#, но можно использовать делегаты с "анонимными методами" (эквивалент встроенных агентов), как в примере:

r (delegate (string s) {return lettercount (s);} ); 

Здесь используется анонимный метод. Заметьте, что анонимный метод объявляет сигнатуру, но не объявляет тип результата. Хотя в C# нет прямого эквивалента Eiffel-понятия "открытый аргумент", но анонимные методы позволяют достичь того же результата.

Эквивалентом встроенного агента Eiffel является лямбда-выражение C#, как в

(int x, int y) => x + y

соответствующее математической записи: ?x, y: INTEGER | x + y.

В C# делегат не ограничен представлением ровно одного метода. Если a и b - делегаты одного типа, то a + b обозначает делегата того же типа. Его выполнение приводит к последовательному выполнению методов, связанных с a и b. Операции add и remove позволяют в список методов, связанных с делегатом, добавлять или удалять новые члены. Можно для этого использовать и операцию присваивания += и -=.

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

public event DT1 click;

Здесь click определяется как событие, которое будет обрабатываться делегатом типа DT1.

Этот пример использует новый тип делегата DT1 вместо рассматриваемого ранее D T, так как типы делегатов, обрабатывающих события, обычно являются процедурами, в то время как DT задает класс функций. Если мы собираемся рассматривать щелчки кнопки мыши, то надо передать событию аргументы, задающие координаты мыши, тогда DT1 будет объявлен как:

public delegate void DT1 (int x, int y);

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

click += r;

Здесь r - это метод с подходящей сигнатурой: void r (int x, int y) . Операция += перегружена для списков; она добавляет элемент в список. Заметьте, что можно применить метод r непосредственно, прежде чем явно обернуть его в одежды делегата. Но то, что получается при добавлении в список, является делегатом. Для удаления подписки используется конструкция -=.

Так выполняется подписка. Для публикации события используется схема:

if (click != null) {click (h, v);}

В этом примере h и v задают координаты мыши. Опять-таки, нужно знать о реализации списка, чтобы понимать необходимость теста click != null: если нет делегатов, подписанных на click, то список будет иметь значение null, и вызов click (h, v) станет причиной исключительной ситуации.

Рекомендуемый стиль для обработки аргументов события в .NET состоит в том, чтобы аргумент события был объявлен как потомок библиотечного класса EventArgs. В этом примере можно было бы объявить класс IntPairArgs, наследуемый от EventArgs, и в этом классе объявить два целочисленных поля x и y. Тогда подписываемые методы имели бы форму:

private void rHandler (object sender, IntPairArgs e) {r (e.x, e.y);}

Здесь - как часть того же рекомендуемого стиля - первый аргумент sender представляет целевой объект, второй аргумент e представляет аргументы события. Преимущество в том, что все схемы обработки события выглядят одинаково. Неясно, однако, оправдывает ли это возникающие усложнения: вместо прямого повторного использования методов, существующих в модели, таких как r, необходимо обернуть их в специальный склеивающий код, такой как rHandler.

Наследование

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

Наследование от класса

Вот как класс объявляет другой класс своим родителем:

class L :K
{… Объявления компонентов L …}

Класс L объявил себя наследником K. Следующий пример показывает случай, когда наследуемый класс объявляется универсальным, здесь - с ограниченной универсальностью:

class M<G>:K where G: T
{… Объявления компонентов M …}

Только один класс, здесь - K, может быть объявлен в качестве родителя.

Абстрактные методы и классы

В C# можно объявлять абстрактными как отдельные методы, так и класс в целом (это соответствует отложенным методам и классам Eiffel, но без контрактов):

abstract class N {
   public abstract void r(); // Заметьте, реализация отсутствует 
   public abstract int s(); // Заметьте, реализация отсутствует {…} 
   … Другие методы, которые могут быть или не быть абстрактными…
}

Нельзя использовать конструктор для создания экземпляров абстрактного класса (как new N (…)). Абстрактный метод может появиться только в абстрактном классе. Неабстрактные потомки должны обеспечить переопределение абстрактных методов родителя. Нельзя одновременно объявить метод класса абстрактным и статическим.

Методы-свойства и индексаторы также могут быть абстрактными.