Виртуальные, переопределяющие и абстрактные методы
Если объявление метода экземпляра содержит модификатор virtual, метод является виртуальным методом. Если модификатор virtual отсутствует, метод называется невиртуальным методом.
При вызове виртуального метода тип времени выполнения экземпляра, для которого осуществляется вызов, определяет фактическую реализацию вызываемого метода. При вызове невиртуального метода определяющим фактором является тип времени компиляции экземпляра.
Виртуальный метод может быть переопределен в производном классе. Если объявление метода экземпляра содержит модификатор override, метод переопределяет унаследованный виртуальный метод с такой же сигнатурой. Объявление виртуального метода представляет новый метод. Объявление переопределяющего метода уточняет существующий виртуальный метод, предоставляя его новую реализацию.
Абстрактным называется виртуальный метод без реализации. Объявление абстрактного метода осуществляется с использованием модификатора abstract и допускается только в классе, объявленном как abstract. В каждом неабстрактном производном классе необходимо переопределять абстрактный метод.
В следующем примере объявляется абстрактный класс Expression, представляющий узел дерева выражений, а также три производных класса: Constant, VariableReference и Operation, которые реализуют узлы дерева выражений для констант, ссылок на переменные и арифметических операций. (Эти классы похожи на типы дерева выражений, представленные в разделе §4.6. Однако их не следует путать.)
using System;
using System.Collections;
public abstract class Expression
{
public abstract double Evaluate(Hashtable vars);
}
public class Constant: Expression
{
double value;
public Constant(double value) {
this.value = value;
}
public override double Evaluate(Hashtable vars) {
return value;
}
}
public class VariableReference: Expression
{
string name;
public VariableReference(string name) {
this.name = name;
}
public override double Evaluate(Hashtable vars) {
object value = vars[name];
if (value == null) {
throw new Exception("Unknown variable: " + name);
}
return Convert.ToDouble(value);
}
}
public class Operation: Expression
{
Expression left;
char op;
Expression right;
public Operation(Expression left, char op, Expression right) {
this.left = left;
this.op = op;
this.right = right;
}
public override double Evaluate(Hashtable vars) {
double x = left.Evaluate(vars);
double y = right.Evaluate(vars);
switch (op) {
case '+': return x + y;
case '-': return x - y;
case '*': return x * y;
case '/': return x / y;
}
throw new Exception("Unknown operator");
}
}
Четыре приведенных выше класса могут использоваться для моделирования арифметических выражений. Например, с помощью экземпляров этих классов выражение x + 3 можно представить следующим образом.
Expression e = new Operation(
new VariableReference("x"),
'+',
new Constant(3));
Метод Evaluate экземпляра Expression вызывается для вычисления заданного выражения и возвращает значение типа double. Метод принимает в качестве аргумента параметр Hashtable, содержащий имена переменных (в качестве ключей записей) и значения (в качестве значений записей). Метод Evaluate представляет собой виртуальный абстрактный метод. Это означает, что в производных от него неабстрактных классах необходимо переопределить этот метод и предоставить его фактическую реализацию.
Реализация Constant метода Evaluate лишь возвращает хранящееся значение константы. Реализация VariableReference осуществляет поиск имени переменной в хэш-таблице и возвращает значение результата. Реализация Operation сначала выполняет вычисление левого и правого операндов (посредством рекурсивного вызова соответствующих методов Evaluate), а затем выполняет заданную арифметическую операцию.
В следующей программе классы Expression используются для вычисления выражения x * (y + 2) с различными значениями x и y.
using System;
using System.Collections;
class Test
{
static void Main() {
Expression e = new Operation(
new VariableReference("x"),
'*',
new Operation(
new VariableReference("y"),
'+',
new Constant(2)
)
);
Hashtable vars = new Hashtable();
vars["x"] = 3;
vars["y"] = 5;
Console.WriteLine(e.Evaluate(vars)); // Outputs "21"
vars["x"] = 1.5;
vars["y"] = 9;
Console.WriteLine(e.Evaluate(vars)); // Outputs "16.5"
}
}
Перегрузка метода
Перегрузка метода позволяет использовать в одном классе несколько методов с одинаковыми именами и различными сигнатурами. При компиляции вызова перегруженного метода компилятор использует разрешение перегрузки для определения конкретного вызываемого метода. С помощью разрешения перегрузки определяется метод, наиболее подходящий для заданных аргументов, или, если такой метод не найден, возвращается сообщение об ошибке. В следующем примере показано действие разрешения перегрузки. В комментариях к каждому вызову метода Main указывается, какой метод фактически вызывается.
class Test
{
static void F() {
Console.WriteLine("F()");
}
static void F(object x) {
Console.WriteLine("F(object)");
}
static void F(int x) {
Console.WriteLine("F(int)");
}
static void F(double x) {
Console.WriteLine("F(double)");
}
static void F<T>(T x) {
Console.WriteLine("F<T>(T)");
}
static void F(double x, double y) {
Console.WriteLine("F(double, double)");
}
static void Main() {
F(); // Invokes F()
F(1); // Invokes F(int)
F(1.0); // Invokes F(double)
F("abc"); // Invokes F(object)
F((double)1); // Invokes F(double)
F((object)1); // Invokes F(object)
F<int>(1); // Invokes F<T>(T)
F(1, 1); // Invokes F(double, double) }
}
Как показано в примере, конкретный метод всегда можно выбрать посредством явного приведения аргументов к соответствующим типам параметров или явного предоставления аргументов типа.
Другие члены-функции
Члены класса, содержащие исполняемый код, в совокупности называются члены-функции. В предыдущем разделе описаны методы, представляющие собой простые члены-функции. В этом разделе описываются другие типы членов-функций, поддерживаемые в C#: конструкторы, свойства, индексаторы, события, операторы и деструкторы.
В следующей таблице описывается универсальный класс List<T>, который реализует расширяемый список объектов. Класс содержит несколько примеров наиболее распространенных типов членов-функций.
public class List<T> { | |
const int defaultCapacity = 4; | Константа |
T[] items; int count; | Поля |
public List(int capacity = defaultCapacity) { items = new T[capacity]; } | Конструкторы |
public int Count { get { return count; } } public int Capacity { get { return items.Length; } set { if (value < count) value = count; if (value != items.Length) { T[] newItems = new T[value]; Array.Copy(items, 0, newItems, 0, count); items = newItems; } } } | Свойства |
public T this[int index] { get { return items[index]; } set { items[index] = value; OnChanged(); } } | Индексатор |
public void Add(T item) { if (count == Capacity) Capacity = count * 2; items[count] = item; count++; OnChanged(); } protected virtual void OnChanged() { if (Changed != null) Changed(this, EventArgs.Empty); } public override bool Equals(object other) { return Equals(this, other as List<T>); } static bool Equals(List<T> a, List<T> b) { if (a == null) return b == null; if (b == null || a.count != b.count) return false; for (int i = 0; i < a.count; i++) { if (!object.Equals(a.items[i], b.items[i])) { return false; } } return true; } | Методы |
public event EventHandler Changed; | Событие |
public static bool operator ==(List<T> a, List<T> b) { return Equals(a, b); } public static bool operator !=(List<T> a, List<T> b) { return !Equals(a, b); } | Операторы |
} |
Конструкторы
В C# поддерживаются конструкторы экземпляров и статические конструкторы. Конструктор экземпляра является членом, реализующим действия, необходимые для инициализации экземпляра класса. Статический конструктор представляет собой член, который реализует действие, необходимое для инициализации самого класса при его первой загрузке.
Конструктор объявляется аналогично методу без типа возвращаемого значения и имеет то же имя, что и содержащий его класс. Объявление конструктора, содержащее модификатор static, объявляет статический конструктор. В противном случае объявляется конструктор экземпляра.
Конструкторы экземпляров можно перегружать. Например, в классе List<T> объявляются два конструктора: один без параметров и один, принимающий параметр типа int. Вызов конструктора экземпляра осуществляется с помощью оператора new. В следующем примере выделяются два экземпляра List<string> с использованием каждого из конструкторов класса List.
List<string> list1 = new List<string>();
List<string> list2 = new List<string>(10);
В отличие от других членов, конструкторы экземпляров не могут наследоваться. Класс содержит только те конструкторы экземпляров, которые фактически объявлены в нем. Если в классе не объявлен конструктор экземпляров, автоматически используется пустой конструктор без параметров.
Свойства
Свойства представляют собой естественные расширения полей. Как свойства, так и поля являются именованными членами со связанными типами, для обращения к которым используется одинаковый синтаксис. Однако в отличие от полей, свойства не указывают места хранения. Вместо этого свойства содержат методы доступа, определяющие операторы, которые используются при чтении или записи их значений.
Свойство объявляется аналогично полю, однако объявление свойства должно заканчиваться не точкой с запятой, а методами доступа get или set, записанными между разделителями — { и }. Свойство, для которого определены оба метода доступа get и set, называется свойством для чтения и записи. Свойство, для которого определен только метод доступа get, называется свойством только для чтения. Свойство, для которого определен только метод доступа set, называется свойством только для записи.
Метод доступа get соответствует не содержащему параметров методу, возвращаемое значение которого имеет тип свойства. За исключением случаев, когда свойство является конечным объектом операции присваивания, при ссылке на свойство в выражении вызывается метод доступа get для вычисления значения свойства.
Метод доступа set соответствует методу с одним параметром value, не имеющему типа возвращаемого значения. При ссылке на свойство как на целевой объект операции присваивания или как на операнд операторов ++ и -- метод доступа set вызывается с аргументом, который предоставляет новое значение.
В классе List<T> объявляются два свойства: Count и Capacity (только для чтения и только для записи соответственно). В следующем примере показано использование этих свойств.
List<string> names = new List<string>();
names.Capacity = 100; // Invokes set accessor
int i = names.Count; // Invokes get accessor
int j = names.Capacity; // Invokes get accessor
Как и в случае с полями и методами, в C# поддерживаются свойства экземпляров и статические свойства. Свойства, объявленные с использованием модификатора static, называются статическими. Все остальные свойства называются свойствами экземпляров.
Методы доступа свойства могут быть виртуальными. Если объявление свойства содержит модификатор virtual, abstract или override, соответствующий тип применяется и к его методам доступа.
Индексаторы
Индексатор — это член, предназначенный для индексации объектов (аналогично массивам). Индексатор объявляется аналогично свойству, однако в качестве имени члена используется this, за которым следует список параметров, записанный между разделителями — [ и ]. Параметры доступны в методах доступа индексатора. Как и свойства, индексаторы могут быть доступны только на чтение, только на запись или на чтение и запись. Методы доступа индексатора могут быть виртуальными.
В классе List объявляется один индексатор для чтения и записи, принимающий параметр int. С его помощью обеспечивается индексация экземпляров класса List с использованием значений типа int. Пример
List<string> names = new List<string>();
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
for (int i = 0; i < names.Count; i++) {
string s = names[i];
names[i] = s.ToUpper();
}
Индексаторы могут быть перегружены. Это означает, что в классе можно объявить несколько индексаторов, различающихся числом или типом параметров.
События
Событие — это член, используемый классом или объектом для предоставления уведомлений. Событие объявляется аналогично полю, однако оно должно иметь тип делегата и его объявление должно содержать ключевое слово event.
Если событие не является абстрактным и не содержит объявления методов доступа, его поведение в классе, в котором оно объявлено, аналогично поведению поля. В поле хранится ссылка на делегат, который представляет обработчики событий, добавленные к событию. Если обработчики событий отсутствуют, поле имеет значение null.
В классе List<T> объявляется член-событие Changed, указывающий на добавление нового элемента в список. Событие Changed вызывается виртуальным методом OnChanged, в котором сначала проверяется, имеет ли событие значение null (т. е. для события отсутствуют обработчики). Понятие вызова события совершенно эквивалентно вызову делегата, представленного событием. Поэтому не существует специальных языковых конструкций для вызова событий.
Реакция клиента на событие реализуется с помощью обработчиков событий. Для добавления обработчиков событий используется оператор «+=, для удаления — оператор -=. В следующем примере к событию Changed класса List<string> присоединяется обработчик событий.
using System;
class Test
{
static int changeCount;
static void ListChanged(object sender, EventArgs e) {
changeCount++;
}
static void Main() {
List<string> names = new List<string>();
names.Changed += new EventHandler(ListChanged);
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
Console.WriteLine(changeCount); // Outputs "3"
}
}
В расширенных сценариях, в которых требуется управление базовым хранилищем события, в объявлении события можно явно определить методы доступа add и remove, которые во многом аналогичны методу доступа set свойства.
Операторы
Оператор — это член, который определяет значение применения конкретного оператора выражения к экземплярам класса. Поддерживается определение операторов трех видов: унарные операторы, двоичные операторы и операторы преобразования. Все операторы должны объявляться с использованием модификаторов public и static.
В классе List<T> объявляются два оператора: operator == и operator !=, что позволяет определить новое значение для выражений, которые применяют эти операторы к экземплярам класса List. В частности, операторы определяют равенство двух экземпляров класса List<T> посредством сравнения всех содержащихся в них объектов с помощью методов Equals. В следующем примере оператор == используется для сравнения двух экземпляров класса List<int>.
using System;
class Test
{
static void Main() {
List<int> a = new List<int>();
a.Add(1);
a.Add(2);
List<int> b = new List<int>();
b.Add(1);
b.Add(2);
Console.WriteLine(a == b); // Outputs "True"
b.Add(3);
Console.WriteLine(a == b); // Outputs "False"
}
}
Первое выражение Console.WriteLine возвращает True, поскольку два списка содержат одинаковое число объектов с одинаковыми значениями и в одинаковом порядке. Если бы в классе List<T> не был определен оператор operator ==, первое выражение Console.WriteLine возвращало бы False, поскольку a и b ссылаются на различные экземпляры класса List<int>.
Деструкторы
Деструктор является членом, реализующим действия, необходимые для уничтожения экземпляра класса. Деструкторы не могут иметь параметров или модификаторов доступа и не могут быть вызваны явно. Деструктор экземпляра вызывается автоматически в процессе сборки мусора.
Сборщик мусора обладает широкими возможностями по определению момента сборки объектов и запуска деструкторов. В частности, выбор времени для вызова деструктора не является определенным. Деструкторы могут выполняться в любом потоке. По этим и другим причинам реализация деструкторов в классе должна осуществляться только при отсутствии других возможных решений.
Наиболее эффективный подход к уничтожению объектов обеспечивается при использовании оператора using.
Структуры
Как и классы, структуры представляют собой структуры данных, содержащие члены-данные и члены-функции. Однако, в отличие от классов, структуры имеют тип значений и не требуют выделения памяти в куче. Переменная типа структуры непосредственно хранит данные структуры, тогда как переменная типа класса хранит ссылку на динамически выделяемый объект. Типы структуры не поддерживают определяемое пользователем наследование. Все типы структуры неявно наследуются от типа object.
Структуры особенно удобны для работы с небольшим объемом данных, имеющих семантику значения. Примерами структур являются комплексные числа, точки в системе координат или словарные пары «ключ-значение». Применение структур вместо классов при работе с небольшими структурами данных позволяет добиться значительной экономии выделяемой памяти и повышения производительности приложения. Например, в следующей программе создается и инициализируется массив из 100 точек. Если в качестве класса реализуется Point, создается 101 экземпляр объектов — один для массива и по одному для каждого из 100 его элементов.
class Point
{
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
class Test
{
static void Main() {
Point[] points = new Point[100];
for (int i = 0; i < 100; i++) points[i] = new Point(i, i);
}
}
Также можно реализовать Point как структуру.
struct Point
{
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
В этом случае создается только один экземпляр объекта для массива. Экземпляры Point хранятся встроенными в массив.
Конструктор структур вызывается с помощью оператора new, однако при этом не предполагается выделение памяти. Вместо динамического выделения объектов с возвращением ссылок на них конструктор структуры возвращает значение структуры (обычно во временной ячейке стека), которое затем при необходимости копируется.
Две переменные типа класса могут ссылаться на один объект. Это позволяет изменять объект, на который ссылается одна переменная, выполняя соответствующие операции с другой. Каждая переменная типа структуры содержит собственную копию данных. В связи с этим операции с одной переменной не влияют на другую. Например, выходные данные следующего фрагмента кода зависят от того, является ли Point классом или структурой.
Point a = new Point(10, 10);
Point b = a;
a.x = 20;
Console.WriteLine(b.x);
Если Point является классом, возвращается значение 20, поскольку a и b ссылаются на один объект. Если Point является структурой, возвращается значение 10, поскольку при присваивании a экземпляру b создается копия значения, на которую не влияет последующее присваивание a.x.
В предыдущем примере показаны два ограничения, накладываемые на структуры. Во-первых, копирование всей структуры обычно менее эффективно, чем копирование ссылки на объект, из-за чего операции присваивания и передачи параметров значения для структур потребляют больше ресурсов, чем аналогичные операции для ссылочных типов. Во-вторых, не поддерживается создание ссылок на структуры (за исключением параметров ref и out), что в некоторых случаях не позволяет использовать структуры.
Массивы
Массив представляет собой структуру данных, содержащую определенное число переменных, доступ к которым осуществляется с помощью вычисляемых индексов. Все переменные, содержащиеся в массиве, которые также называются элементами массива, имеют одинаковый тип, который называется типом элементов массива.
Типы массивов являются ссылочными типами, поэтому при объявлении переменной типа массива выделяется память для ссылки на экземпляр массива. Фактические экземпляры массива создаются динамически во время выполнения с помощью оператора new. Оператор new определяет длину создаваемого экземпляра массива, которая остается неизменной в течение всего времени существования экземпляра. Элементам массива присваиваются индексы в диапазоне от 0 до Length - 1. Оператор new автоматически инициализирует элементы массива с использованием значений по умолчанию (нули для всех числовых типов или null для всех ссылочных типов).
В следующем примере создается и инициализируется массив элементов типа int, после чего выводится содержимое созданного массива.
using System;
class Test
{
static void Main() {
int[] a = new int[10];
for (int i = 0; i < a.Length; i++) {
a[i] = i * i;
}
for (int i = 0; i < a.Length; i++) {
Console.WriteLine("a[{0}] = {1}", i, a[i]);
}
}
}
В этом примере создается и используется одномерный массив. В C# также поддерживаются многомерные массивы. Число измерений также называется рангом типа массива и определяется как сумма единицы и числа запятых, указанных в квадратных скобках типа массива. В следующем примере выделяется память для одно-, двух- и трехмерного массивов.
int[] a1 = new int[10];
int[,] a2 = new int[10, 5];
int[,,] a3 = new int[10, 5, 2];
Массив a1 содержит 10 элементов, массив a2 — 50 (10 × 5) элементов, а массив a3 — 100 (10 × 5 × 2) элементов.
Элемент массива может иметь любой тип, в том числе тип массива. Массив, содержащий элементы типа массива (jagged array, или массив массивов), иногда называется неравномерным массивом, поскольку его элементы могут иметь различную длину. В следующем примере выделяется память для массива массивов типа int:
int[][] a = new int[3][];
a[0] = new int[10];
a[1] = new int[5];
a[2] = new int[20];
В первой строке создается массив, состоящий из трех элементов типа int[], каждый из которых имеет начальное значение null. В последующих строках инициализируются три элемента со ссылками на отдельные экземпляры массивов различной длины.
Оператор new позволяет задать начальные значения элементов массива с помощью инициализатора массива, который представляет собой список выражений, записанных между разделителями { и }. В следующем примере выделяется и инициализируется массив int[], содержащий три элемента.
int[] a = new int[] {1, 2, 3};
Обратите внимание, что длина массива определяется на основании числа выражений между фигурными скобками { и }. Поддерживается сокращенная форма объявления локальных переменных и полей, что позволяет не указывать повторно тип массива.
int[] a = {1, 2, 3};
Оба предыдущих примера эквивалентны следующему:
int[] t = new int[3];
t[0] = 1;
t[1] = 2;
t[2] = 3;
int[] a = t;
Интерфейсы
Интерфейс определяет контракт, который может быть реализован классами и структурами. Интерфейс может содержать методы, свойства, события и индексаторы. Интерфейс не предоставляет реализацию определяемых в нем членов. Вместо этого в нем задаются члены, которые должны предоставляться классами или структурами, реализующими такой интерфейс.
Для интерфейсов допускается множественное наследование. В следующем примере интерфейс IComboBox одновременно наследуется от интерфейсов ITextBox и IListBox.
interface IControl
{
void Paint();
}
interface ITextBox: IControl
{
void SetText(string text);
}
interface IListBox: IControl
{
void SetItems(string[] items);
}
interface IComboBox: ITextBox, IListBox {}
Классы и структуры способны реализовывать несколько интерфейсов. В следующем примере класс EditBox одновременно реализует интерфейсы IControl и IDataBound.
interface IDataBound
{
void Bind(Binder b);
}
public class EditBox: IControl, IDataBound
{
public void Paint() {...}
public void Bind(Binder b) {...}
}
Если класс или структура реализует конкретный интерфейс, экземпляры такого класса или такой структуры могут быть неявно преобразованы к типу заданного интерфейса. Пример
EditBox editBox = new EditBox();
IControl control = editBox;
IDataBound dataBound = editBox;
Если статически неизвестно, реализует ли экземпляр конкретный интерфейс, могут использоваться динамические приведения типов. Например, следующие операторы используют динамические приведения типов для получения реализаций интерфейсов IControl и IDataBound объекта. Поскольку фактическим типом объекта является EditBox, приведения выполняются успешно.
object obj = new EditBox();
IControl control = (IControl)obj;
IDataBound dataBound = (IDataBound)obj;
В представленном выше классе EditBox метод Paint интерфейса IControl и метод Bind интерфейса IDataBound реализуются с использованием членов с модификатором public. В C# также поддерживается явная реализация членов интерфейса, что позволяет не использовать в классах и структурах члены public. Явная реализация члена интерфейса записывается с использованием полного имени члена интерфейса. Например, класс EditBox может использовать явные реализации членов интерфейса для реализации методов IControl.Paint и IDataBound.Bind следующим образом.
public class EditBox: IControl, IDataBound
{
void IControl.Paint() {...}
void IDataBound.Bind(Binder b) {...}
}
Обращение к явным членам интерфейса осуществляется исключительно посредством типа интерфейса. Например, реализация метода IControl.Paint, предоставляемая показанным выше классом EditBox, может быть вызвана только с предварительным преобразованием ссылки на EditBox к типу интерфейса IControl.
EditBox editBox = new EditBox();
editBox.Paint(); // Error, no such method
IControl control = editBox;
control.Paint(); // Ok
Перечисления
Перечисляемый тип представляет собой тип значений, содержащий конечное число именованных констант. В следующем примере объявляется и используется перечисляемый тип Color, содержащий три постоянных значения: Red, Green и Blue.
using System;
enum Color
{
Red,
Green,
Blue
}
class Test
{
static void PrintColor(Color color) {
switch (color) {
case Color.Red:
Console.WriteLine("Red");
break;
case Color.Green:
Console.WriteLine("Green");
break;
case Color.Blue:
Console.WriteLine("Blue");
break;
default:
Console.WriteLine("Unknown color");
break;
}
}
static void Main() {
Color c = Color.Red;
PrintColor(c);
PrintColor(Color.Blue);
}
}
Каждый перечисляемый тип имеет соответствующий целый тип, называемый базовым типом перечисляемого типа. Если для перечисляемого типа явно не объявлен базовый тип, в качестве базового используется тип int. Формат хранения и диапазон возможных значений перечисляемого типа определяются его базовым типом. Набор значений перечисляемого типа может включать его члены, но не ограничивается только ими. В частности, любое значение базового типа может быть приведено к перечисляемому типу и является отдельным допустимым значением такого типа.
В следующем примере объявляется перечисляемый тип Alignment с базовым типом sbyte:
enum Alignment: sbyte
{
Left = -1,
Center = 0,
Right = 1
}
Как показано в приведенном выше примере, объявление члена перечисляемого типа может включать константное выражение, определяющее значение такого члена. Значение константы для каждого члена должно принадлежать диапазону базового типа. Если в объявлении перечисляемого типа значение не задается явно, первому члену типа присваивается нулевое значение, а каждому последующему члену — значение текстуально предшествующего ему члена, увеличенное на единицу.
Значения перечисляемых типов могут быть преобразованы к значениям целых типов (и наоборот) с помощью приведений типов. Пример
int i = (int)Color.Blue; // int i = 2;
Color c = (Color)2; // Color c = Color.Blue;
В качестве значения по умолчанию для любого перечисляемого типа используется целое нулевое значение, преобразованное к перечисляемому типу. Если переменные автоматически инициализируются с использованием значений по умолчанию, такие значения присваиваются переменным перечисляемого типа. Чтобы обеспечить доступность значений по умолчанию перечисляемого типа, литерал 0 неявно преобразуется к любому перечисляемому типу. Таким образом, допускаются выражения следующего вида.
Color c = 0;
Делегаты
Тип делегата представляет собой ссылки на методы с конкретным списком параметров и типом возвращаемого значения. С помощью делегатов методы обрабатываются как сущности, которым можно передавать параметры и которые можно присваивать переменным. Понятие делегата близко к понятию указателя на функцию, используемому в некоторых других языках. Однако делегаты, в отличие от указателей на функции, представляют собой пример объектно-ориентированного и типобезопасного подхода к программированию.
В следующем примере объявляется и используется тип делегата Function.
using System;
delegate double Function(double x);
class Multiplier
{
double factor;
public Multiplier(double factor) {
this.factor = factor;
}
public double Multiply(double x) {
return x * factor;
}
}
class Test
{
static double Square(double x) {
return x * x;
}
static double[] Apply(double[] a, Function f) {
double[] result = new double[a.Length];
for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
return result;
}
static void Main() {
double[] a = {0.0, 0.5, 1.0};
double[] squares = Apply(a, Square);
double[] sines = Apply(a, Math.Sin);
Multiplier m = new Multiplier(2.0);
double[] doubles = Apply(a, m.Multiply);
}
}
Экземпляр типа делегата Function может ссылаться на любой метод, который принимает аргумент типа double и возвращает значение типа double. Метод Apply применяет заданную функцию Function к элементам массива типа double[] и возвращает массив типа double[], содержащий результаты. В методе Main метод Apply используется для применения трех различных функций к массиву типа double[].
Делегат может ссылаться как на статический метод (например, Square или Math.Sin в предыдущем примере), так и на метод экземпляра (например, m.Multiply в предыдущем примере). Делегат, ссылающийся на метод экземпляра, также ссылается на конкретный объект. При вызове такого метода экземпляра с помощью делегата этот объект становится объектом this.
Делегаты также могут создаваться с помощью анонимных функций, которые представляют собой «встроенные методы», создаваемые в процессе выполнения. Анонимные функции могут видеть локальные переменные окружающих их методов. Таким образом, приведенный выше пример множителя может быть записан проще с использованием класса Multiplier:
double[] doubles = Apply(a, (double x) => x * 2.0);
Интересной и полезной особенностью делегата является то, что для него неважен тип класса или метода, на который он ссылается. Единственным требованием является наличие у метода, на который ссылается делегат, такого же числа параметров и типа возвращаемого значения.
Атрибуты
Типы, члены и другие сущности C# поддерживают модификаторы, которые управляют определенными аспектами их поведения. Например, доступность метода управляется модификаторами public, protected, internal и private. Благодаря этой возможности в C# пользовательские типы декларативных сведений могут быть вложены в сущности программы и извлекаться во время выполнения. Такие дополнительные декларативные сведения задаются в программе посредством определения и использования атрибутов.
В следующем примере атрибут HelpAttribute присоединяется к сущностям программы и предоставляет ссылки на связанную с ними документацию.
using System;
public class HelpAttribute: Attribute
{
string url;
string topic;
public HelpAttribute(string url) {
this.url = url;
}
public string Url {
get { return url; }
}
public string Topic {
get { return topic; }
set { topic = value; }
}
}
Все классы атрибутов наследуются от базового класса System.Attribute, предоставляемого платформой .NET Framework. Чтобы применить атрибут, необходимо указать его имя и любые другие аргументы в квадратных скобках непосредственно перед связанным объявлением. Если имя атрибута заканчивается словом Attribute, при ссылке на него эту часть имени можно опустить. Например, атрибут HelpAttribute можно использовать следующим образом.
[Help("http://msdn.microsoft.com/.../MyClass.htm")]
public class Widget
{
[Help("http://msdn.microsoft.com/.../MyClass.htm", Topic = "Display")]
public void Display(string text) {}
}
В этом примере атрибут HelpAttribute присоединяется к классу Widget, а другой атрибут HelpAttribute — к методу Display класса. Общие конструкторы класса атрибута управляют сведениями, которые предоставляются при вложении атрибута в сущность программы. Дополнительные сведения предоставляются посредством ссылки на открытые свойства для чтения и записи класса атрибута (например, ссылка на свойства Topic в предыдущем примере).
В следующем примере показывается извлечение сведений атрибута для заданной сущности программы во время выполнения с помощью отражения.
using System;
using System.Reflection;
class Test
{
static void ShowHelp(MemberInfo member) {
HelpAttribute a = Attribute.GetCustomAttribute(member,
typeof(HelpAttribute)) as HelpAttribute;
if (a == null) {
Console.WriteLine("No help for {0}", member);
}
else {
Console.WriteLine("Help for {0}:", member);
Console.WriteLine(" Url={0}, Topic={1}", a.Url, a.Topic);
}
}
static void Main() {
ShowHelp(typeof(Widget));
ShowHelp(typeof(Widget).GetMethod("Display"));
}
}
При запросе конкретного атрибута с использованием отражения вызывается конструктор класса атрибута с данными, предоставленными в исходном коде программы, и возвращается результирующий экземпляр атрибута. Если с помощью свойств предоставлены дополнительные сведения, соответствующие значения присваиваются свойствам до возвращения экземпляра атрибута.
Лексическая структура
Программы
Программа на C# состоит из одного или более исходных файлов, формально называемых единицами компиляции (§9.1). Исходный файл — это упорядоченная последовательность символов Юникода. Исходные файлы обычно взаимнооднозначно соответствуют файлам файловой системы, но это соответствие не является обязательным. Для максимальной переносимости рекомендуется использовать для файлов в файловой системе кодировку UTF-8.
С концептуальной точки зрения программа компилируется в три этапа:
1. Преобразование: файл преобразуется из конкретного набора символов и схемы кодировки в последовательность символов Юникода.
2. Лексический анализ: поток входных символов Юникода преобразуется в поток лексем.
3. Синтаксический анализ: поток лексем преобразуется в исполняемый код.
Грамматики
В настоящей спецификации представлен синтаксис языка программирования C#, использующий две грамматики. Лексическая грамматика (§2.2.2) определяет, как объединяются символы Юникода для образования признаков конца строки, пробелов, комментариев, лексем и препроцессорных директив. Синтаксическая грамматика (§2.2.3) определяет, как объединяются лексемы, полученные от лексической грамматики, для образования программ на C#.
Грамматическая нотация
Лексическая и синтаксическая грамматики представлены с помощью грамматических порождений. Каждое грамматическое порождение определяет нетерминальный символ и возможные подстановки этого нетерминального символа в последовательности нетерминальных или терминальных символов. В грамматических порождениях нетерминальные символы отображаются курсивом, а символы terminal — моноширинным шрифтом.
Первой строкой грамматического порождения является имя определяемого нетерминального символа, за которым следует двоеточие. Каждая последующая строка с отступом содержит возможную подстановку нетерминального символа, данную в виде последовательности нетерминальных или терминальных символов. Например, порождение:
while-statement:
while ( boolean-expression ) embedded-statement
определяет оператор while как состоящий из лексемы while, последующей лексемы "(", последующего логического выражения, последующей лексемы ")" и последующего внедренного оператора.
Если существует более одной возможной подстановки нетерминального символа, варианты перечисляются в отдельных строках. Например, порождение:
statement-list:
statement
statement-list statement
определяет список операторов как состоящий или из оператора, или из списка операторов с последующим оператором. Иначе говоря, это определение является рекурсивным и указывает, что список операторов состоит из одного или более операторов.
Подстрочный суффикс "opt" используется для указания необязательного символа. Порождение:
block:
{ statement-listopt }
является краткой записью для:
block:
{ }
{ statement-list }
и определяет блок как состоящий из необязательного списка операторов, заключенного в лексемы "{" и "}".
Варианты обычно перечисляются в отдельных строках, но если вариантов много, фраза «one of» («одно из») может предшествовать списку подстановок, заданных в одной строке. Это просто краткая запись вместо перечисления каждого из вариантов отдельной строкой. Например, порождение:
real-type-suffix: one of
F f D d M m
является краткой записью для:
real-type-suffix:
F
f
D
d
M
m
Лексика
Лексическая грамматика C# представлена в разделах §2.3, §2.4 и §2.5. Терминальными символами лексической грамматики являются символы набора символов Юникода. Лексическая грамматика определяет, как символы объединяются для образования лексем (§2.4), пробелов (§2.3.3), комментариев (§2.3.2) и препроцессорных директив (§2.5).
Каждый исходный файл программы на C# должен соответствовать порождению ввода лексической грамматики (§2.3).
Синтаксическая грамматика
Синтаксическая грамматика C# представлена в главах и приложениях, следующих за этой главой. Терминальными символами синтаксической грамматики являются лексемы, определенные лексической грамматикой. Синтаксическая грамматика определяет, как лексемы объединяются для образования программ на C#.
Каждый исходный файл в программе на C# должен соответствовать порождению единицы компиляции синтаксической грамматики (§9.1).
Лексический анализ
Порождение ввода определяет лексическую структуру исходного файла на C#. Каждый исходный файл в программе на C# должен соответствовать этому порождению лексической грамматики.
input:
input-sectionopt
input-section:
input-section-part
input-section input-section-part
input-section-part:
input-elementsopt new-line
pp-directive
input-elements:
input-element
in