Россия |
Введение в 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 на , сдвиг вправо - делению.
Можно использовать перечисленные знаки для определения операций в собственных классах, исключением являются знаки полустрогих операций. Механизм перегрузки используется и для операций. В случае операций сравнения перегрузка должна идти парами - если перегружается операция "меньше" (<), то необходимо определить и "больше" (> ). Аналогично и для других операций сравнения.
Кроме того, 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 (…)). Абстрактный метод может появиться только в абстрактном классе. Неабстрактные потомки должны обеспечить переопределение абстрактных методов родителя. Нельзя одновременно объявить метод класса абстрактным и статическим.
Методы-свойства и индексаторы также могут быть абстрактными.