Статические события и события экземпляров
Если в объявление события включен модификатор static, событие называют статическим событием. Если нет модификатора static, событие называется событием экземпляра.
Статическое событие не связано с конкретным экземпляром, и обращение к this в методах доступа статического события является ошибкой времени компиляции.
Событие экземпляра связано с данным экземпляром класса, и к этому экземпляру можно обращаться как к this (§7.6.7) в методах доступа этого события.
Когда к событию обращаются через доступ к члену (§7.6.4) вида E.M, если M является статическим событием, E должно означать тип, содержащий M, а если M является событием экземпляра, E должно означать экземпляр типа, содержащего M.
Различия между статическими членами и членами экземпляров рассматриваются в разделе §10.3.7.
Виртуальные, запечатанные, переопределяющие и абстрактные методы доступа
Объявление события как virtual указывает, что методы доступа этого события являются виртуальными. Модификатор virtual применяется к обоим методам доступа события.
Объявление события как abstract указывает, что методы доступа события являются виртуальными, но не предоставляет фактическую реализацию методов доступа. Вместо этого требуются не абстрактные производные классы для предоставления их собственной реализации для методов доступа посредством переопределения события. Поскольку объявление абстрактного события не предоставляет фактическую реализацию, оно не может предоставить разделенные скобками объявления методов доступа к событиям.
Объявление события, включающее оба модификатора abstract и override, указывает, что событие является абстрактным и переопределяет основное событие. Методы доступа такого события также являются абстрактными.
Объявления абстрактных событий разрешены только в абстрактных классах (§10.1.1.1).
Методы доступа унаследованного виртуального события могут быть переопределены в производном классе включением объявления события, указывающего модификатор override. Это называется объявлением переопределяющего события. Объявление переопределяющего события не объявляет новое событие. Вместо этого оно просто специализирует реализации методов доступа существующего виртуального события.
Объявление переопределяющего события должно указывать точно те же модификаторы доступа, тип и имя, что и переопределяемое событие.
Объявление переопределяющего события может включать модификатор sealed. Использование этого модификатора предотвращает дальнейшее переопределение события производным классом. Методы доступа запечатанного события также запечатаны.
Включение модификатора new в объявление переопределяющего события является ошибкой времени компиляции.
За исключением различий в синтаксисе объявления и вызова, поведение виртуальных, запечатанных, переопределяющих или абстрактных методов доступа в точности соответствует поведению виртуальных, запечатанных, переопределяющих или абстрактных методов. В частности, правила, описанные в разделах §10.6.3, §10.6.4, §10.6.5 и §10.6.6, применяются, как если бы методы доступа были методами соответствующего вида. Каждый метод доступа соответствует методу с единственным параметром-значением типа события, с типом возвращаемого значения void и с теми же модификаторами, что содержащее событие.
Индексаторы
Индексатор является членом, дающим возможность индексировать объект так же, как массив. Индексаторы объявляются с помощью объявлений индексаторов:
indexer-declaration:
attributesopt indexer-modifiersopt indexer-declarator { accessor-declarations }
indexer-modifiers:
indexer-modifier
indexer-modifiers indexer-modifier
indexer-modifier:
new
public
protected
internal
private
virtual
sealed
override
abstract
extern
indexer-declarator:
type this [ formal-parameter-list ]
type interface-type . this [ formal-parameter-list ]
Объявление индексатора может включать набор атрибутов (§17) допустимое сочетание любых из четырех модификаторов доступа (§10.3.5), а также модификаторы new (§10.3.4), virtual (§10.6.3), override (§10.6.4), sealed (§10.6.5), abstract (§10.6.6) и extern (§10.6.7).
Объявления индексаторов подчиняются тем же правилам, что и объявления методов (§10.6), в отношении допустимых сочетаний модификаторов, с одним исключением – модификатор static не допускается в объявлении индексатора.
Модификаторы virtual, override и abstractявляются взаимоисключающими, кроме одного случая. Модификаторы abstract и override могут использоваться совместно, так чтобы абстрактный индексатор мог переопределить виртуальный.
Тип в объявлении индексатора указывает тип элемента индексатора, вводимого объявлением. Если только индексатор не является явной реализацией члена интерфейса, за типом следует зарезервированное слово this. Для явной реализации члена интерфейса за типом следует тип интерфейса, «.» и зарезервированное слово this. В отличие от других членов, индексаторы не имеют имен, определяемых пользователем.
Список формальных параметров указывает параметры индексатора. Список формальных параметров индексатора соответствует списку формальных параметров метода (§10.6.1), кроме того, что по крайней мере один параметр должен быть указан, а модификаторы параметров ref и out не разрешены.
Тип индексатора и каждый из типов, на которые есть ссылки в списке формальных параметров, должны быть по крайней мере так же доступными, как сам индексатор (§3.5.4).
Объявления методов доступа (§10.7.2), которые должны быть заключены в лексемы "{" и "}", объявляют методы доступа индексатора. Методы доступа указывают исполняемые операторы, связанные с чтением и записью элементов индексатора.
Хотя синтаксис доступа к элементу индексатора такой же, как синтаксис доступа к элементу массива, элемент индексатора не классифицируется как переменная. Так, невозможно передать элемент индексатора в качестве аргумента ref или out.
Список формальных параметров индексатора определяет подпись (§3.6) индексатора. В частности, подпись индексатора состоит из числа и типов его формальных параметров. Тип элемента и имена формальных параметров не являются частью сигнатуры индексатора.
Сигнатура индексатора должна отличаться от сигнатур всех других индексаторов, объявленных в этом же классе.
Понятия индексаторов и свойств очень схожи, но отличаются следующим:
· свойство идентифицируется своим именем, а индексатор идентифицируется своей подписью;
· доступ к свойству производится через простое имя (§7.6.2) или доступ к члену (§7.6.4), а доступ к элементу индексатора производится через доступ к элементу (§7.6.6.2);
· свойство может быть членом static, а индексатор всегда является членом экземпляра;
· метод доступа get свойства соответствует методу без параметров, тогда как метод доступа get индексатора соответствует методу с тем же списком формальных параметров, что у индексатора;
· метод доступа set свойства соответствует методу с одним параметром с именем value, тогда как метод доступа set индексатора соответствует методу с тем же списком формальных параметров, что у индексатора, плюс дополнительный параметр с именем value;
· объявление в методе доступа к индексатору локальной переменной с тем же именем, что и параметр индексатора, является ошибкой времени компиляции;
· в объявлении переопределяющего свойства при обращении к унаследованному свойству используется синтаксис base.P, где P – имя свойства. В объявлении переопределяющего индексатора при обращении к унаследованному индексатору используется синтаксис base[E], где E – список выражений, разделенных запятыми.
За исключением этих различий, все правила, определенные в §10.7.2 и §10.7.3, применяются к методам доступа к индексатору, а также к методам доступа к свойствам.
Если в объявление индексатора включен модификатор extern, индексатор называется внешним индексатором. Так как объявление внешнего индексатора не предоставляет фактической реализации, каждое из его объявлений метода доступа состоит из точки с запятой.
В примере внизу объявлен класс BitArray, реализующий индексатор для доступа к отдельным битам в битовом массиве.
using System;
class BitArray
{
int[] bits;
int length;
public BitArray(int length) {
if (length < 0) throw new ArgumentException();
bits = new int[((length - 1) >> 5) + 1];
this.length = length;
}
public int Length {
get { return length; }
}
public bool this[int index] {
get {
if (index < 0 || index >= length) {
throw new IndexOutOfRangeException();
}
return (bits[index >> 5] & 1 << index) != 0;
}
set {
if (index < 0 || index >= length) {
throw new IndexOutOfRangeException();
}
if (value) {
bits[index >> 5] |= 1 << index;
}
else {
bits[index >> 5] &= ~(1 << index);
}
}
}
}
Экземпляр класса BitArray расходует существенно меньше памяти, чем соответствующий bool[] (так как каждое значение первого занимает только один бит, тогда как у второго – один байт), но позволяет выполнять те же операции, что и bool[].
В следующем классе CountPrimes используется BitArray и классический алгоритм «решето» для вычисления количества простых чисел между 1 и заданным максимумом.
class CountPrimes
{
static int Count(int max) {
BitArray flags = new BitArray(max + 1);
int count = 1;
for (int i = 2; i <= max; i++) {
if (!flags[i]) {
for (int j = i * 2; j <= max; j += i) flags[j] = true;
count++;
}
}
return count;
}
static void Main(string[] args) {
int max = int.Parse(args[0]);
int count = Count(max);
Console.WriteLine("Found {0} primes between 1 and {1}", count, max);
}
}
Обратите внимание, что синтаксис обращения к элементам BitArray точно такой же, как для bool[].
В следующем примере показан класс сетки 26 ´ 10 с индексатором с двумя параметрами. Первым параметром должна быть буква верхнего или нижнего регистра в диапазоне A–Z, а вторым – целое число в диапазоне 0–9.
using System;
class Grid
{
const int NumRows = 26;
const int NumCols = 10;
int[,] cells = new int[NumRows, NumCols];
public int this[char c, int col] {
get {
c = Char.ToUpper(c);
if (c < 'A' || c > 'Z') {
throw new ArgumentException();
}
if (col < 0 || col >= NumCols) {
throw new IndexOutOfRangeException();
}
return cells[c - 'A', col];
}
set {
c = Char.ToUpper(c);
if (c < 'A' || c > 'Z') {
throw new ArgumentException();
}
if (col < 0 || col >= NumCols) {
throw new IndexOutOfRangeException();
}
cells[c - 'A', col] = value;
}
}
}
Перегрузка индексатора
Правила разрешения перегрузки индексаторов описаны в §7.5.2.
Операторы
Оператор является членом, определяющим значение оператора выражения, который можно применить к экземплярам класса. Операторы объявляются с помощью объявлений операторов:
operator-declaration:
attributesopt operator-modifiers operator-declarator operator-body
operator-modifiers:
operator-modifier
operator-modifiers operator-modifier
operator-modifier:
public
static
extern
operator-declarator:
unary-operator-declarator
binary-operator-declarator
conversion-operator-declarator
unary-operator-declarator:
type operator overloadable-unary-operator ( type identifier )
overloadable-unary-operator: one of
+ - ! ~ ++ -- true false
binary-operator-declarator:
type operator overloadable-binary-operator ( type identifier , type identifier )
overloadable-binary-operator:
+
-
*
/
%
&
|
^
<<
right-shift
==
!=
>
<
>=
<=
conversion-operator-declarator:
implicit operator type ( type identifier )
explicit operator type ( type identifier )
operator-body:
block
;
Есть три категории перегружаемых операторов: унарные операторы (§10.10.1), бинарные операторы (§10.10.2) и операторы преобразования (§10.10.3).
Если в объявление оператора включен модификатор extern, оператор называется внешним оператором. Так как внешний оператор не предоставляет фактическую реализацию, его тело оператора состоит из точки с запятой. Для всех других операторов тело оператора состоит из блока, в котором указаны операторы, выполняемые при вызове этого оператора. Блок оператора должен удовлетворять правилам для методов, возвращающих значение, описанным в §10.6.10.
Следующие правила применяются ко всем объявлениям операторов:
· объявление оператора должно включать оба модификатора: public и static;
· параметрами оператора должны быть параметры-значения (§5.1.4). Указание в объявлении оператора параметров ref или out является ошибкой времени компиляции;
· сигнатура оператора (§10.10.1, §10.10.2, §10.10.3) должна отличаться от сигнатур всех других операторов, объявленных в этом же классе;
· все типы, на которые ссылается объявление оператора, должны быть, по крайней мере, так же доступными, как сам оператор (§3.5.4);
· неоднократное появление одного и того же модификатора в объявлении оператора является ошибкой времени компиляции.
Каждая категория операторов налагает дополнительные ограничения, как описано в следующих разделах.
Подобно другим членам, операторы, объявленные в базовом классе, наследуются производными классами. Так как объявления операторов всегда требуют участия класса или структуры, где объявлен оператор, в сигнатуре оператора, то для оператора, объявленного в производном классе, невозможно скрыть оператор, объявленный в базовом классе. Таким образом, модификатор new никогда не требуется и поэтому никогда не допускается в объявлении оператора.
Дополнительные сведения об унарных и бинарных операторах можно найти в §7.3.
Дополнительные сведения об операторах преобразования можно найти в §6.4.
Унарные операторы.
К объявлениям унарных операторов применяются следующие правила, здесь T обозначает тип экземпляра класса или структуры, где содержится объявление оператора:
· унарный оператор +, -, ! или ~ должен принимать единственный параметр типа T или T? и может возвращать любой тип;
· унарный оператор ++ или -- должен принимать единственный параметр типа T или T? и должен возвращать тот же самый тип или тип, производный от него;
· унарный оператор true или false должен принимать единственный параметр типа T или T? и возвращать значение типа bool.
Подпись унарного оператора состоит из лексемы оператора (+, -, !, ~, ++, --, true или false) и типа единственного формального параметра. Тип возвращаемого значения не является частью сигнатуры унарного оператора, как и имя формального параметра.
Для унарных операторов true и false требуется попарное объявление. Если в классе объявлен один из этих операторов без объявления другого, возникает ошибка времени компиляции. Операторы true и false описаны далее в разделах §7.12.2 и §7.20.
В следующем примере показана реализация и последующее применение operator ++ для класса целочисленного вектора.
public class IntVector
{
public IntVector(int length) {...}
public int Length {...} // read-only property
public int this[int index] {...} // read-write indexer
public static IntVector operator ++(IntVector iv) {
IntVector temp = new IntVector(iv.Length);
for (int i = 0; i < iv.Length; i++)
temp[i] = iv[i] + 1;
return temp;
}
}
class Test
{
static void Main() {
IntVector iv1 = new IntVector(4); // vector of 4 x 0
IntVector iv2;
iv2 = iv1++; // iv2 contains 4 x 0, iv1 contains 4 x 1
iv2 = ++iv1; // iv2 contains 4 x 2, iv1 contains 4 x 2
}
}
Обратите внимание, как метод оператора возвращает значение, созданное добавлением 1 к операнду, точно так же, как операторы постфиксного увеличения и уменьшения (§7.6.9) и операторы префиксного увеличения и уменьшения (§7.7.5). В отличие от C++, этому методу не требуется непосредственно изменять значение своего операнда. Фактически изменение значения операнда нарушала бы стандартную семантику оператора постфиксного увеличения.
Бинарные операторы
К объявлениям бинарных операторов применяются следующие правила, здесь T обозначает тип экземпляра класса или структуры, где содержится объявление оператора:
· бинарный оператор не-сдвига должен принимать два параметра, по крайней мере один из которых должен иметь тип T или T?, и может возвращать любой тип;
· бинарный оператор << или >> должен принимать два параметра, первый из которых должен иметь тип T или T?, а второй должен иметь тип int или int? и может возвращать любой тип.
Подпись бинарного оператора состоит из лексемы оператора (+, -, *, /, %, &, |, ^, <<, >>, ==, !=, >, <, >= или <=) и типов этих двух формальных параметров. Тип возвращаемого значения и имена формальных параметров не являются частью сигнатуры бинарного оператора.
Для некоторых бинарных операторов требуется попарное объявление. Для каждого объявления одного из операторов пары должно быть соответствующее объявление другого оператора из пары. Два объявления операторов соответствуют, если у них одинаковый тип возвращаемого значения и одинаковый тип каждого параметра. Для следующих операторов требуется попарное объявление:
· operator == и operator !=
· operator > и operator <
· operator >= и operator <=
10.10.3 Операторы преобразования
Объявление оператора преобразования вводит пользовательское преобразование (§6.4), которое дополняет предопределенные неявные и явные преобразования.
Объявление оператора преобразования, включающее зарезервированное слово implicit, вводит неявное преобразование, определенное пользователем. Неявные преобразования могут происходить в разнообразных ситуациях, включая вызовы членов функций, приведение выражений и присваивания. Это описано далее в §6.1.
Объявление оператора преобразования, включающее зарезервированное слово explicit, вводит явное преобразование, определенное пользователем. Явные преобразования могут происходить в выражениях приведения, они описаны далее в §6.2.
Оператор преобразования выполняет преобразование из исходного типа, указанного типом параметра оператора преобразования, в конечный тип, указанный возвращаемым типом оператора преобразования.
Для данного исходного типа S и конечного типа T, если S или T является типом, допускающим присваивание пустой ссылки, пусть S0 и T0 ссылаются на свои основные типы, иначе S0 и T0 равны S и T соответственно. В классе или структуре разрешено объявлять преобразование от исходного типа S к конечному типу T, если только справедливо все следующее:
· S0 и T0 являются разными типами;
· либо S0, либо T0 является типом структуры или класса, где имеет место объявление этого оператора;
· ни S0, ни T0 не является типом интерфейса;
· без преобразований, определенных пользователем, не существует преобразование от S к T или от T к S.
Для выполнения этих правил любые параметры типа, связанные с S или T, считаются уникальными типами, не имеющими отношений наследования с другими типами, а любые ограничения на эти параметры типа игнорируются.
В этом примере
class C<T> {...}
class D<T>: C<T>
{
public static implicit operator C<int>(D<T> value) {...} // Ok
public static implicit operator C<string>(D<T> value) {...} // Ok
public static implicit operator C<T>(D<T> value) {...} // Error
}
первые два объявления операторов разрешены, так как, по замыслу §10.9.3, T, int и string соответственно считаются уникальными типами без отношений. Однако третий оператор ошибочен, так как C<T> является базовым классом для D<T>.
Из второго правила следует, что оператор преобразования должен преобразовывать либо в направлении к типу класса или структуры, либо в направлении от типа класса или структуры, где оператор объявлен. Например, можно для типа класса или структуры C определить преобразование из C в int и из int в C, но не из int в bool.
Невозможно непосредственно переопределить предопределенное преобразование. Так, операторам преобразования запрещено преобразовывать от или к object, так как уже существуют неявные и явные преобразования между object и всеми другими типами Аналогично, ни исходный, ни конечный тип преобразования не может быть базовым типом другого, так как в этом случае преобразование уже существует.
Однако возможно объявить операторы для универсальных типов, которые для аргументов особого типа указывают преобразования, уже существующие как предопределенные. В этом примере
struct Convertible<T>
{
public static implicit operator Convertible<T>(T value) {...}
public static explicit operator T(Convertible<T> value) {...}
}
если тип object указан в качестве аргумента типа для T, второй оператор объявляет преобразование, которое уже существует (неявное и, следовательно, также и явное преобразование существует от любого типа к типу object).
В тех случаях, когда существует предопределенное преобразование между двумя типами, любые пользовательские преобразования между этими типами игнорируются. А именно:
· если существует неявное предопределенное преобразование (§6.1) от типа S к типу T, все определенные пользователем преобразования (неявные или явные) от S к T игнорируются;
· если существует явное предопределенное преобразование (§6.2) от типа S к типу T, любые пользовательские явные преобразования от S к T игнорируются. Более того:
o если T является типом интерфейса, пользовательские неявные преобразования от S к T игнорируются.
o В противном случае пользовательские неявные преобразования от S к T все же признаются.
Для всех типов, кроме object, операторы, объявленные типом Convertible<T> вверху, не противоречат предопределенным преобразованиям. Пример:
void F(int i, Convertible<int> n) {
i = n; // Error
i = (int)n; // User-defined explicit conversion
n = i; // User-defined implicit conversion
n = (Convertible<int>)i; // User-defined implicit conversion
}
Однако для типа object предопределенные преобразования скрывают пользовательские преобразования во всех случаях, кроме одного:
void F(object o, Convertible<object> n) {
o = n; // Pre-defined boxing conversion
o = (object)n; // Pre-defined boxing conversion
n = o; // User-defined implicit conversion
n = (Convertible<object>)o; // Pre-defined unboxing conversion
}
Пользовательским преобразованиям запрещено преобразование из типов интерфейса или в типы интерфейса. В частности, это ограничение обеспечивает отсутствие каких-либо пользовательских преобразований при преобразовании в тип интерфейса; кроме того, оно гарантирует, что преобразование в тип интерфейса будет успешно выполнено, только если преобразуемый объект действительно реализует указанный тип интерфейса.
Сигнатура оператора преобразования состоит из исходного типа и конечного типа. (Обратите внимание, что это единственный вид члена, в котором тип возвращаемого значения участвует в сигнатуре). Классификация implicit или explicit оператора преобразования не является частью подписи оператора. Так, в классе или структуре нельзя объявить операторы преобразования и implicit, и explicit с теми же исходными и конечными типами.
В общем, пользовательские неявные преобразования следует разрабатывать так, чтобы они никогда не вызывали исключения и не теряли информацию. Если определенное пользователем преобразование может вызывать исключения (например, потому что исходный аргумент вне допустимого диапазона) или терять информацию (например, отбрасывать старшие разряды), то такое преобразование следует определить как явное преобразование.
В этом примере
using System;
public struct Digit
{
byte value;
public Digit(byte value) {
if (value < 0 || value > 9) throw new ArgumentException();
this.value = value;
}
public static implicit operator byte(Digit d) {
return d.value;
}
public static explicit operator Digit(byte b) {
return new Digit(b);
}
}
В этом примере преобразование от Digit к byte неявное, так как оно никогда не вызывает исключения и не теряет информацию, а преобразование от byte к Digit явное, так как Digit может представлять только поднабор возможных значений byte.
Конструкторы экземпляров
Конструктор экземпляра является членом, реализующим действия, необходимые для инициализации экземпляра класса. Конструкторы экземпляров объявляются с помощью объявлений конструкторов:
constructor-declaration:
attributesopt constructor-modifiersopt constructor-declarator constructor-body
constructor-modifiers:
constructor-modifier
constructor-modifiers constructor-modifier
constructor-modifier:
public
protected
internal
private
extern
constructor-declarator:
identifier ( formal-parameter-listopt ) constructor-initializeropt
constructor-initializer:
: base ( argument-listopt )
: this ( argument-listopt )
constructor-body:
block
;
Объявление конструктора может включать набор атрибутов (§17), допустимое сочетание из четырех модификаторов доступа (§10.3.5) и модификатор extern (§10.6.7). В объявление конструктора запрещено включать один и тот же модификатор несколько раз.
Идентификатор в деклараторе конструктора должен указывать имя класса, в котором объявлен конструктор экземпляров. Если указано любое другое имя, происходит ошибка времени компиляции.
Необязательный список формальных параметров конструктора экземпляров подчиняется тем же правилам, что и список формальных параметров метода (§10.6). Список формальных параметров определяет подпись (§3.6) конструктора экземпляров и управляет процессом, посредством которого разрешение перегрузки (§7.5.2) выбирает отдельный конструктор экземпляров в вызове.
Каждый тип, на который есть ссылка в списке формальных параметров конструктора экземпляров, должен быть, по крайней мере, так же доступным, как сам конструктор (§3.5.4).
Необязательный инициализатор конструктора указывает другой конструктор экземпляров для вызова перед выполнением операторов, заданных в теле конструктора этого конструктора экземпляров. Это описано далее в §10.11.1.
Если в объявление конструктора включен модификатор extern, конструктор называется внешним конструктором. Поскольку объявление внешнего конструктора не предоставляет фактическую реализацию, его тело конструктора состоит из точки с запятой. Во всех других конструкторах тело конструктора состоит из блока, в котором указаны операторы для инициализации нового экземпляра класса. Это в точности соответствует блоку экземпляра метода с типом возвращаемого значения void (§10.6.10).
Конструкторы экземпляров не наследуются. Таким образом, у класса нет конструкторов экземпляров, отличных от тех, что действительно объявлены в этом классе. Если класс не содержит объявления конструкторов экземпляров, автоматически предоставляется конструктор экземпляров по умолчанию (§10.11.4).
Конструкторы экземпляров вызываются выражениями создания объектов (§7.6.10.1) и посредством инициализаторов конструкторов.