Поделиться через


15 классов

15.1 Общие

Класс — это структура данных, которая может содержать элементы данных (константы и поля), члены функции (методы, свойства, события, индексаторы, операторы, конструкторы экземпляров, методы завершения и статические конструкторы) и вложенные типы. Типы классов поддерживают наследование, механизм, в котором производный класс может расширять и специализировать базовый класс.

Объявления классов 15.2

15.2.1 Общие

Class_declaration — это type_declaration (§14.7), которая объявляет новый класс.

class_declaration
    : attributes? class_modifier* 'partial'? 'class' identifier
        type_parameter_list? class_base? type_parameter_constraints_clause*
        class_body ';'?
    ;

Class_declaration состоит из необязательного набора атрибутов(§22partial), а затем ключевое слово и class, который называет класс, за которым следует необязательный type_parameter_list (§15.2.3), за которым следует необязательное class_base спецификации (§15.2.4), за которым следует необязательный набор type_parameter_constraints_clause (§15.2.5), за которым следует class_body (§15.2.6), за которым следует точка с запятой.

Объявление класса не должно предоставлять type_parameter_constraints_clause, если оно также не предоставляет type_parameter_list.

Объявление класса, которое предоставляет type_parameter_list , является универсальным объявлением класса. Кроме того, любой класс, вложенный в объявление универсального класса или универсальное объявление структуры, является объявлением универсального класса, так как аргументы типов для содержащего типа должны быть предоставлены для создания созданного типа (§8.4).

Модификаторы классов 15.2.2

15.2.2.1 Общие

При необходимости class_declaration может содержать последовательность модификаторов классов:

class_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'abstract'
    | 'sealed'
    | 'static'
    | unsafe_modifier   // unsafe code support
    ;

unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).

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

Модификатор new разрешен для вложенных классов. Он указывает, что класс скрывает унаследованный член по тому же имени, как описано в §15.3.5. Это ошибка во время компиляции для модификатора, new отображаемого в объявлении класса, которое не является вложенным объявлением класса.

Модификаторы publicи , protectedinternalprivate а также управляют специальными возможностями класса. В зависимости от контекста, в котором происходит объявление класса, некоторые из этих модификаторов могут быть запрещены (§7.5.2).

Если объявление частичного типа (§15.2.7) включает спецификацию специальных возможностей (private Если часть частичного типа не включает спецификацию специальных возможностей, тип получает соответствующую доступность по умолчанию (§7.5.2).

Модификаторы abstractи sealed модификаторы staticрассматриваются в следующих подклаузах.

15.2.2.2 Абстрактные классы

Модификатор abstract используется для указания того, что класс является неполным и предназначен для использования только в качестве базового класса. Абстрактный класс отличается от не абстрактного класса следующим образом:

  • Абстрактный класс нельзя создать непосредственно, и это ошибка во время компиляции для использования new оператора в абстрактном классе. Хотя можно иметь переменные и значения, типы времени компиляции которых являются абстрактными, такие переменные и значения обязательно будут либо null содержать ссылки на экземпляры не абстрактных классов, производных от абстрактных типов.
  • Абстрактный класс разрешен (но не требуется) для хранения абстрактных элементов.
  • Абстрактный класс нельзя запечатывать.

Если не абстрактный класс является производным от абстрактного класса, не абстрактный класс должен включать фактические реализации всех унаследованных абстрактных элементов, тем самым переопределяя эти абстрактные члены.

Пример. В следующем коде

abstract class A
{
    public abstract void F();
}

abstract class B : A
{
    public void G() {}
}

class C : B
{
    public override void F()
    {
        // Actual implementation of F
    }
}

Абстрактный класс A представляет абстрактный метод F. Класс B вводит дополнительный метод G, но так как он не предоставляет реализацию F, B также должен быть объявлен абстрактным. Класс C переопределяет F и предоставляет фактическую реализацию. Поскольку в ней нет абстрактных элементов C, C разрешено (но не обязательно) быть не абстрактными.

пример конца

Если одна или несколько частей объявления частичного типа (§15.2.7) класса включают abstract модификатор, класс является абстрактным. В противном случае класс является не абстрактным.

15.2.2.3 Запечатанные классы

Модификатор sealed используется для предотвращения производных от класса. Ошибка во время компиляции возникает, если запечатанный класс указан как базовый класс другого класса.

Запечатанный класс также не может быть абстрактным классом.

Примечание.sealed Модификатор в основном используется для предотвращения непреднамеренного производных, но также включает определенные оптимизации во время выполнения. В частности, поскольку закрытый класс, как известно, никогда не имеет производных классов, можно преобразовать вызовы виртуальных функций в запечатанные экземпляры классов в не виртуальные вызовы. конечная заметка

Если одна или несколько частей объявления частичного типа (§15.2.7) класса включают sealed модификатор, класс запечатывается. В противном случае класс неуправляем.

Статические классы 15.2.2.4

15.2.2.4.1 Общие

Модификатор static используется для обозначения класса, объявленного как статический класс. Статический класс не должен быть создан, не должен использоваться в качестве типа и содержать только статические элементы. Только статический класс может содержать объявления методов расширения (§15.6.10).

Объявление статического класса имеет следующие ограничения:

  • Статический sealed класс не должен включать или abstract модификатор. (Однако, поскольку статический класс не может быть создан или производный от него, он ведет себя так, как если бы он был запечатан и абстрагирован.)
  • Статический класс не должен содержать спецификацию class_base (§15.2.4) и не может явно указывать базовый класс или список реализованных интерфейсов. Статический класс неявно наследует от типа object.
  • Статический класс должен содержать только статические элементы (§15.3.8).

    Примечание. Все константы и вложенные типы классифицируются как статические элементы. конечная заметка

  • Статический класс не должен содержать члены со protectedспециальными private protectedвозможностями или protected internal объявленными специальными возможностями.

Это ошибка во время компиляции для нарушения любого из этих ограничений.

Статический класс не имеет конструкторов экземпляров. Не удается объявить конструктор экземпляра в статичном классе, и для статического класса конструктор экземпляра по умолчанию (§15.11.5) не предоставляется для статического класса.

Члены статического класса не являются автоматически статическими, а объявления-члены должны явно включать static модификатор (за исключением констант и вложенных типов). Если класс вложен в статический внешний класс, вложенный класс не является статическим, если он явно не включает static модификатор.

Если одна или несколько частей объявления частичного типа (§15.2.7) класса включают static модификатор, класс является статическим. В противном случае класс не является статическим.

15.2.2.4.2, ссылающиеся на типы статических классов

Namespace_or_type_name (§7.8) разрешено ссылаться на статический класс, если

  • Namespace_or_type_name — это T namespace_or_type_name формы T.Iили
  • Имя namespace_or_type — это T typeof_expression (§12.8.18) формыtypeof(T).

Primary_expression (§12.8) разрешено ссылаться на статический класс, если

  • Primary_expression представляет собой Emember_access (§12.8.7) формыE.I.

В любом другом контексте это ошибка во время компиляции для ссылки на статический класс.

Примечание. Например, это ошибка для статического класса, используемого в качестве базового класса, составного типа (§15.3.7) элемента, аргумента универсального типа или ограничения параметра типа. Аналогичным образом статический класс нельзя использовать в типе массива, новом выражении, выражении приведения, выражением как выражением, выражением как выражением, sizeof выражением или выражением значения по умолчанию. конечная заметка

Параметры типа 15.2.3

Параметр типа — это простой идентификатор, обозначающий заполнитель для аргумента типа, предоставленного для создания созданного типа. По ограничению аргумент типа (§8.4.2) является типом, заменяемым параметром типа при создании созданного типа.

type_parameter_list
    : '<' type_parameters '>'
    ;

type_parameters
    : attributes? type_parameter
    | type_parameters ',' attributes? type_parameter
    ;

type_parameter определен в §8.5.

Каждый параметр типа в объявлении класса определяет имя в пространстве объявления (§7.3) этого класса. Таким образом, он не может иметь то же имя, что и другой параметр типа этого класса или член, объявленный в этом классе. Параметр типа не может иметь то же имя, что и сам тип.

Два частичных объявления универсального типа (в одной программе) способствуют одному и тому же универсальному типу, если у них есть одно полное имя (которое включает generic_dimension_specifier (§12.8.18) для числа параметров типа) (§7.8.3). Два таких объявления частичных типов должны указывать одинаковое имя для каждого параметра типа в порядке.

Базовая спецификация класса 15.2.4

15.2.4.1 Общие

Объявление класса может включать спецификацию class_base , которая определяет прямой базовый класс класса и интерфейсы (§18) непосредственно реализованные классом.

class_base
    : ':' class_type
    | ':' interface_type_list
    | ':' class_type ',' interface_type_list
    ;

interface_type_list
    : interface_type (',' interface_type)*
    ;

Базовые классы 15.2.4.2

Если class_type включен в class_base, он указывает прямой базовый класс объявленного класса. Если объявление не частичного класса не имеет class_base или если class_base перечисляет только типы интерфейсов, предполагается, что objectпрямой базовый класс. Если объявление частичного класса содержит спецификацию базового класса, спецификация базового класса должна ссылаться на тот же тип, что и все остальные части этого частичного типа, включающие спецификацию базового класса. Если часть частичного класса не включает спецификацию базового класса, базовый класс .object Класс наследует элементы от своего прямого базового класса, как описано в §15.3.4.

Пример. В следующем коде

class A {}
class B : A {}

Класс A , как говорят, является прямым базовым классом B, и B , как говорят, является производным от A. Поскольку A явно не указывает прямой базовый класс, его прямой базовый класс неявно object.

пример конца

Для созданного типа класса, включая вложенный тип, объявленный в объявлении универсального типа (§15.3.9.7), если базовый класс указан в объявлении универсального класса, базовый класс созданного типа получается путем подстановки для каждого type_parameter в объявлении базового класса, соответствующего type_argument созданного типа.

Пример. Учитывая объявления универсального класса

class B<U,V> {...}
class G<T> : B<string,T[]> {...}

базовый класс созданного типа G<int> будет B<string,int[]>.

пример конца

Базовый класс, указанный в объявлении класса, может быть созданным типом класса (§8.4). Базовый класс не может быть параметром типа самостоятельно (§8.5), хотя он может включать параметры типа, которые находятся в области.

Пример:

class Base<T> {}

// Valid, non-constructed class with constructed base class
class Extend1 : Base<int> {}

// Error, type parameter used as base class
class Extend2<V> : V {}

// Valid, type parameter used as type argument for base class
class Extend3<V> : Base<V> {}

пример конца

Прямой базовый класс типа класса должен быть по крайней мере таким же доступным, как сам тип класса (§7.5.5). Например, это ошибка во время компиляции для общедоступного класса, наследуемого от частного или внутреннего класса.

Прямой базовый класс типа класса не должен иметь ни одного из следующих типов: System.Array, System.DelegateSystem.EnumSystem.ValueTypeили dynamic типа. Кроме того, объявление универсального класса не должно использоваться System.Attribute в качестве прямого или косвенного базового класса (§22.2.1).

При определении смысла спецификации A прямого базового класса класса Bпредполагается временное Bопределение прямого базового класса, что гарантирует, что значение спецификации базового класса object не может рекурсивно зависеть от себя.

Пример. Ниже приведено следующее

class X<T>
{
    public class Y{}
}

class Z : X<Z.Y> {}

является ошибкой, так как в спецификации X<Z.Y> базового класса прямой базовый класс Z считается object, и поэтому (по правилам §7.8) Z не считается членом Y.

пример конца

Базовые классы класса — это прямой базовый класс и его базовые классы. Другими словами, набор базовых классов является транзитивным закрытием прямой связи базового класса.

Пример. В следующем примере:

class A {...}
class B<T> : A {...}
class C<T> : B<IComparable<T>> {...}
class D<T> : C<T[]> {...}

базовые D<int> классы: C<int[]>, B<IComparable<int[]>>Aи object.

пример конца

За исключением класса object, каждый класс имеет ровно один прямой базовый класс. Класс object не имеет прямого базового класса и является конечным базовым классом всех других классов.

Это ошибка во время компиляции для класса, который будет зависеть от самого себя. В целях этого правила класс напрямую зависит от его прямого базового класса (если такового) и напрямую зависит от ближайшего заключиющего класса, в котором он вложен (при наличии). Учитывая это определение, полный набор классов, от которых зависит класс, является транзитивным закрытием непосредственно зависит от связи.

Пример: пример

class A : A {}

является ошибочным, так как класс зависит от самого себя. Аналогичным образом, пример

class A : B {}
class B : C {}
class C : A {}

возникает ошибка, так как классы циклно зависят от себя. Наконец, пример

class A : B.C {}
class B : A
{
    public class C {}
}

приводит к ошибке во время компиляции, так как A зависит от (его прямой базовый класс), который зависит от B.C (его немедленного заключенного класса), от которого циклиально зависит BA.

пример конца

Класс не зависит от классов, вложенных в него.

Пример. В следующем коде

class A
{
    class B : A {}
}

B A зависит от (поскольку это как прямой базовый класс, так и его немедленно заключенный класс), но A не зависит A от (так как BB ни базовый класс, ни вложенный классA). Таким образом, пример является допустимым.

пример конца

Невозможно наследовать от запечатаного класса.

Пример. В следующем коде

sealed class A {}
class B : A {} // Error, cannot derive from a sealed class

Класс B находится в ошибке, так как он пытается наследоваться от запечатаемого класса A.

пример конца

Реализации интерфейса 15.2.4.3

Спецификация class_base может содержать список типов интерфейса, в этом случае класс, как сообщается, реализует указанные типы интерфейсов. Для созданного типа класса, включая вложенный тип, объявленный в объявлении универсального типа (§15.3.9.7), каждый реализованный тип интерфейса получается путем подстановки для каждого type_parameter в данном интерфейсе, соответствующего type_argument созданного типа.

Набор интерфейсов для типа, объявленного в нескольких частях (§15.2.7), является объединением интерфейсов, указанных для каждой части. Определенный интерфейс можно назвать только один раз в каждой части, но несколько частей могут называть одни и те же базовые интерфейсы. Существует только одна реализация каждого члена любого заданного интерфейса.

Пример. В следующем примере:

partial class C : IA, IB {...}
partial class C : IC {...}
partial class C : IA, IB {...}

Набор базовых интерфейсов для класса CIA, IBи IC.

пример конца

Как правило, каждая часть предоставляет реализацию интерфейсов, объявленных в этой части; однако это не обязательно. Часть может предоставить реализацию для интерфейса, объявленного в другой части.

Пример:

partial class X
{
    int IComparable.CompareTo(object o) {...}
}

partial class X : IComparable
{
    ...
}

пример конца

Базовые интерфейсы, указанные в объявлении класса, можно создать типы интерфейсов (§8.4, §18.2). Базовый интерфейс не может быть параметром типа самостоятельно, хотя он может включать параметры типа, которые находятся в области.

Пример. В следующем коде показано, как класс может реализовать и расширить созданные типы:

class C<U, V> {}
interface I1<V> {}
class D : C<string, int>, I1<string> {}
class E<T> : C<int, T>, I1<T> {}

пример конца

Реализации интерфейса рассматриваются далее в §18.6.

Ограничения параметров типа 15.2.5

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

type_parameter_constraints_clauses
    : type_parameter_constraints_clause
    | type_parameter_constraints_clauses type_parameter_constraints_clause
    ;

type_parameter_constraints_clause
    : 'where' type_parameter ':' type_parameter_constraints
    ;

type_parameter_constraints
    : primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
    | secondary_constraints (',' constructor_constraint)?
    | constructor_constraint
    ;

primary_constraint
    : class_type nullable_type_annotation?
    | 'class' nullable_type_annotation?
    | 'struct'
    | 'notnull'
    | 'unmanaged'
    ;

secondary_constraint
    : interface_type nullable_type_annotation?
    | type_parameter nullable_type_annotation?
    ;

secondary_constraints
    : secondary_constraint (',' secondary_constraint)*
    ;

constructor_constraint
    : 'new' '(' ')'
    ;

Каждый type_parameter_constraints_clause состоит из маркера where, за которым следует имя параметра типа, за которым следует двоеточие и список ограничений для этого параметра типа. Для каждого параметра типа может быть не более одного where предложения, и where предложения могут быть перечислены в любом порядке. get Как и set маркеры в методе доступа к свойствам, where маркер не является ключевым словом.

Список ограничений, указанных в where предложении, может включать в себя любой из следующих компонентов: одно основное ограничение, одно или несколько дополнительных ограничений и ограничение конструктора. new()

Основное ограничение может быть типом класса, unmanaged Тип класса и ограничение ссылочного типа могут включать nullable_type_annotation.

Дополнительное ограничение может быть interface_type или type_parameter, за которым следует nullable_type_annotation. Наличие nullable_type_annotatione* указывает, что аргумент типа может быть ссылочным типом, допускающим значение NULL, который соответствует типу ссылок, не допускающим значения NULL, который удовлетворяет ограничению.

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

Тип класса, ограничение ссылочного типа и вторичные ограничения могут включать заметку типа NULL. Наличие или отсутствие этой заметки в параметре типа указывает ожидания null для аргумента типа:

  • Если ограничение не включает заметку типа null, аргумент типа, как ожидается, будет не допускающим значение NULL ссылочным типом. Компилятор может выдавать предупреждение, если аргумент типа является ссылочным типом, допускающим значение NULL.
  • Если ограничение включает заметку типа NULL, ограничение удовлетворяется как типом ссылок, не допускающим значение NULL, так и ссылочным типом, допускающим значение NULL.

Значение NULL аргумента типа не совпадает со значением NULL параметра типа. Компилятор может выдавать предупреждение, если значение NULL параметра типа не соответствует значению NULL аргумента типа.

Примечание. Чтобы указать, что аргумент типа является ссылочным типом, допускающим значение NULL, не добавляйте заметку типа NULL в качестве ограничения (используйте T : class или T : BaseClass), но используйте T? в универсальном объявлении для указания соответствующего ссылочного типа, допускающего значение NULL, для аргумента типа. конечная заметка

Заметка типа, допускающего значение NULL, ?не может использоваться для аргумента неограниченного типа.

Для параметра T типа, если аргумент типа является ссылочным типом C?, допускающего значение NULL, экземпляры T? интерпретируются как C?, а не C??.

Пример. В следующих примерах показано, как значение NULL аргумента типа влияет на значение NULL объявления его параметра типа:

public class C
{
}

public static class  Extensions
{
    public static void M<T>(this T? arg) where T : notnull
    {

    }
}

public class Test
{
    public void M()
    {
        C? mightBeNull = new C();
        C notNull = new C();

        int number = 5;
        int? missing = null;

        mightBeNull.M(); // arg is C?
        notNull.M(); //  arg is C?
        number.M(); // arg is int?
        missing.M(); // arg is int?
    }
}

Если аргумент типа является типом, не допускающим значение NULL, ? заметка типа указывает, что параметр является соответствующим типом, допускающим значение NULL. Если аргумент типа уже является ссылочным типом, допускающим значение NULL, параметр является тем же типом, допускающим значение NULL.

пример конца

Ограничение null указывает, что аргумент типа, используемый для параметра типа, должен быть типом ненулевого значения или не допускающим значение null типом ссылок. Аргумент типа, который не является ненулевым типом значения или ненулевым ссылочным типом, допускается, но компилятор может выдать диагностическое предупреждение.

Ограничение типа значения указывает, что аргумент типа, используемый для параметра типа, должен быть типом значения, не допускающим значение NULL. Все типы структур, не допускающие значения NULL, типы перечисления и параметры типа, имеющие ограничение типа значения, удовлетворяют этому ограничению. Обратите внимание, что хотя классифицируется как тип значения, тип значения null (§8.3.12) не удовлетворяет ограничению типа значения. Параметр типа с ограничением типа не должен также иметь constructor_constraint, хотя он может использоваться в качестве аргумента типа для другого параметра типа с constructor_constraint.

Примечание. Тип System.Nullable<T> задает ограничение типа значения, не допускающего значение NULL, для T. Таким образом, рекурсивно построенные типы форм T?? и Nullable<Nullable<T>> запрещены. конечная заметка

Поскольку unmanaged это не ключевое слово, в primary_constraint неуправляемое ограничение всегда синтаксически неоднозначно с class_type. По соображениям совместимости, если подстановка имени (§12.8.4) имени unmanaged будет выполнена class_typeуспешно. В противном случае оно рассматривается как неуправляемые ограничения.

Ограничение неуправляемого типа указывает, что аргумент типа, используемый для параметра типа, должен быть неуправляемым типом (§8.8).

Типы указателей никогда не могут быть аргументами типа и не удовлетворяют ни одному ограничению типа, даже неуправляемому, несмотря на неуправляемые типы.

Если ограничение является типом класса, типом интерфейса или параметром типа, этот тип указывает минимальный базовый тип, который должен поддерживать каждый аргумент типа, используемый для этого параметра типа. При использовании созданного типа или универсального метода аргумент типа проверяется на соответствие ограничениям параметра типа во время компиляции. Указанный аргумент типа должен соответствовать условиям, описанным в разделе 8.4.5.

Ограничение class_type должно соответствовать следующим правилам:

  • Тип должен быть типом класса.
  • Тип не должен быть sealed.
  • Тип не должен быть одним из следующих типов: System.Array или System.ValueType.
  • Тип не должен быть object.
  • По крайней мере одно ограничение для заданного параметра типа может быть типом класса.

Тип, указанный в качестве ограничения interface_type , должен соответствовать следующим правилам:

  • Тип должен быть типом интерфейса.
  • Тип не должен указываться несколько раз в заданном where предложении.

В любом случае ограничение может включать любой из параметров типа связанного типа или объявления метода в составе созданного типа и может включать объявленный тип.

Любой класс или тип интерфейса, указанный в качестве ограничения параметра типа, должен быть по крайней мере доступным (§7.5.5) как объявленный универсальный тип или метод.

Тип, указанный как ограничение type_parameter , должно соответствовать следующим правилам:

  • Тип должен быть параметром типа.
  • Тип не должен указываться несколько раз в заданном where предложении.

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

  • Если параметр T типа используется в качестве ограничения для параметра S типа, то Sзависит отT него.
  • Если параметр S типа зависит от параметра T типа и T зависит от параметра типа, то S негоU.

Учитывая это отношение, это ошибка во время компиляции для параметра типа, который будет зависеть от себя (прямо или косвенно).

Все ограничения должны быть согласованы между зависимыми параметрами типа. Если параметр S типа зависит от параметра T типа, то:

  • T не должно иметь ограничения типа значения. В противном случае фактически запечатанный, T поэтому S он должен быть таким же типом, как Tи при устранении необходимости двух параметров типа.
  • Если S имеет ограничение типа значения, T то не должно быть ограничения class_type .
  • Если S имеет ограничение class_type и A имеет B, то должно быть преобразование удостоверений или неявное преобразование ссылок из AB или неявное преобразование ссылок из BAнего.
  • Если S также зависит от параметра U типа и U имеет ограничение class_type и A имеет TB, то должно быть преобразование удостоверений или неявное преобразование ссылок из AB или неявное преобразование ссылок в .BA

S Допустимо иметь ограничение типа значения и T иметь ограничение ссылочного типа. Фактически это ограничивает T типы System.Object, System.ValueTypeSystem.Enumи любой тип интерфейса.

where Если предложение параметра типа включает ограничение конструктора (которое имеет формуnew()), можно использовать new оператор для создания экземпляров типа (§12.8.17.2). Любой аргумент типа, используемый для параметра типа с ограничением конструктора, должен быть типом значения, не абстрактным классом с открытым конструктором без параметров или параметром типа с ограничением типа или ограничением конструктора.

Это ошибка во время компиляции для type_parameter_constraints наличия primary_constraintstruct или unmanaged наличия constructor_constraint.

Пример: ниже приведены примеры ограничений.

interface IPrintable
{
    void Print();
}

interface IComparable<T>
{
    int CompareTo(T value);
}

interface IKeyProvider<T>
{
    T GetKey();
}

class Printer<T> where T : IPrintable {...}
class SortedList<T> where T : IComparable<T> {...}

class Dictionary<K,V>
    where K : IComparable<K>
    where V : IPrintable, IKeyProvider<K>, new()
{
    ...
}

В следующем примере возникает ошибка, так как это приводит к цикличности в граф зависимостей параметров типа:

class Circular<S,T>
    where S: T
    where T: S // Error, circularity in dependency graph
{
    ...
}

В следующих примерах показаны дополнительные недопустимые ситуации:

class Sealed<S,T>
    where S : T
    where T : struct // Error, `T` is sealed
{
    ...
}

class A {...}
class B {...}

class Incompat<S,T>
    where S : A, T
    where T : B // Error, incompatible class-type constraints
{
    ...
}

class StructWithClass<S,T,U>
    where S : struct, T
    where T : U
    where U : A // Error, A incompatible with struct
{
    ...
}

пример конца

Динамический стирание типа C состоит из Cₓ типа, созданного следующим образом:

  • Если C это вложенный тип Outer.Inner , Cₓ то это вложенный тип Outerₓ.Innerₓ.
  • Если CCₓиспользуется созданный тип G<A¹, ..., Aⁿ> с аргументами A¹, ..., Aⁿ типа, то Cₓ это созданный тип G<A¹ₓ, ..., Aⁿₓ>.
  • Если C это тип массива, E[] то это тип CₓEₓ[]массива.
  • Если C это динамический, то Cₓ есть object.
  • В противном случае Cₓ является C.

Эффективный базовый класс параметра T типа определяется следующим образом:

Давайте рассмотрим R набор типов, которые:

  • Для каждого ограничения этого T параметра типа R содержится его действующий базовый класс.
  • Для каждого ограничения этого T типа структуры R содержится System.ValueType.
  • Для каждого ограничения этого T типа перечисления R содержится System.Enum.
  • Для каждого ограничения этого T типа R делегата содержит динамическую эрастику.
  • Для каждого ограничения этого T типа R массива содержится System.Array.
  • Для каждого ограничения этого T типа R класса содержит динамическую эрастику.

Следующее действие

  • Если T имеет ограничение типа значения, то его действующий базовый класс имеет значение System.ValueType.
  • В противном случае, если R пустой, то эффективный базовый класс object.
  • В противном случае эффективный базовый класс T является наиболее охватываемого типа (§10.5.3) набора R. Если набор не имеет охватываемого типа, то действующий базовый класс T имеет значение object. Правила согласованности гарантируют наличие наиболее охватываемого типа.

Если параметр типа является параметром типа метода, ограничения которого наследуются от базового метода, эффективный базовый класс вычисляется после подстановки типов.

Эти правила гарантируют, что эффективный базовый класс всегда является class_type.

Эффективный набор интерфейсов параметра T типа определяется следующим образом:

  • Если T нет secondary_constraints, его эффективный набор интерфейсов пуст.
  • Если T есть interface_type ограничения, но не type_parameter ограничений, то его эффективный набор интерфейсов представляет собой набор динамических эрустраков interface_type ограничений.
  • Если T нет interface_type ограничений, но имеет ограничения type_parameter, то его эффективный набор интерфейсов является объединением эффективных наборов интерфейсов своих type_parameter ограничений.
  • Если T есть и interface_type ограничения, и ограничения type_parameter, то его эффективный набор интерфейсов — объединение набора динамических ограничений interface_type и эффективных наборов интерфейсов type_parameter ограничений.

Параметр типа, как известно, является ссылочным типом, если он имеет ограничение ссылочного типа или его действующий базовый класс не является илиobject.System.ValueType Параметр типа, как известно, является ненулевым ссылочным типом , если он, как известно, является ссылочным типом и имеет ограничение ссылочного типа, не допускающего значение NULL.

Значения типа параметра ограниченного типа можно использовать для доступа к элементам экземпляра, подразумеваемым ограничениями.

Пример. В следующем примере:

interface IPrintable
{
    void Print();
}

class Printer<T> where T : IPrintable
{
    void PrintOne(T x) => x.Print();
}

Методы IPrintable могут вызываться напрямую, x так как T ограничено для всегда реализации IPrintable.

пример конца

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

Пример:

partial class Map<K,V>
    where K : IComparable<K>
    where V : IKeyProvider<K>, new()
{
    ...
}

partial class Map<K,V>
    where V : IKeyProvider<K>, new()
    where K : IComparable<K>
{
    ...
}

partial class Map<K,V>
{
    ...
}

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

пример конца

Тело класса 15.2.6

Class_body класса определяет члены этого класса.

class_body
    : '{' class_member_declaration* '}'
    ;

15.2.7 Частичные объявления

Модификатор partial используется при определении класса, структуры или типа интерфейса в нескольких частях. Модификатор partial является контекстным ключевым словом (§6.4.4) и имеет специальное значение непосредственно перед одним из ключевых слов class, structили interface.

Каждая часть объявления частичного типа должна включать partial модификатор и должна быть объявлена в том же пространстве имен или содержит тип, что и другие части. Модификатор partial указывает, что дополнительные части объявления типа могут существовать в другом месте, но существование таких дополнительных частей не является обязательным требованием; оно допустимо для единственного объявления типа для включения partial модификатора. Допускается только одно объявление частичного типа, включающее базовый класс или реализованные интерфейсы. Однако все объявления базового класса или реализованные интерфейсы должны соответствовать, включая допустимость значений NULL для всех указанных аргументов типа.

Все части частичного типа должны компилироваться вместе, чтобы части могли быть объединены во время компиляции. Частичные типы, в частности, не позволяют расширить уже скомпилированные типы.

Вложенные типы можно объявить в нескольких частях с помощью partial модификатора. Как правило, содержащий тип объявляется также с помощью partial , и каждая часть вложенного типа объявляется в другой части содержащего типа.

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

public partial class Customer
{
    private int id;
    private string name;
    private string address;
    private List<Order> orders;

    public Customer()
    {
        ...
    }
}

// File: Customer2.cs
public partial class Customer
{
    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

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

public class Customer
{
    private int id;
    private string name;
    private string address;
    private List<Order> orders;

    public Customer()
    {
        ...
    }

    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

пример конца

Обработка атрибутов, указанных в параметрах типа или типа различных частей частичного объявления, рассматривается в §22.3.

Члены класса 15.3

15.3.1 Общие

Члены класса состоят из элементов, введенных его class_member_declarationи членов, унаследованных от прямого базового класса.

class_member_declaration
    : constant_declaration
    | field_declaration
    | method_declaration
    | property_declaration
    | event_declaration
    | indexer_declaration
    | operator_declaration
    | constructor_declaration
    | finalizer_declaration
    | static_constructor_declaration
    | type_declaration
    ;

Члены класса делятся на следующие категории:

  • Константы, представляющие значения констант, связанные с классом (§15.4).
  • Поля, которые являются переменными класса (§15.5).
  • Методы, реализующие вычисления и действия, которые могут выполняться классом (§15.6).
  • Свойства, определяющие именованные характеристики и действия, связанные с чтением и записью этих характеристик (§15.7).
  • События, определяющие уведомления, которые могут быть созданы классом (§15.8).
  • Индексаторы, которые позволяют индексировать экземпляры класса таким же образом (синтаксически) как массивы (§15.9).
  • Операторы, определяющие операторы выражений, которые могут применяться к экземплярам класса (§15.10).
  • Конструкторы экземпляров, реализующие действия, необходимые для инициализации экземпляров класса (§15.11)
  • Методы завершения, реализующие действия, выполняемые до окончательного удаления экземпляров класса (§15.13).
  • Статические конструкторы, реализующие действия, необходимые для инициализации самого класса (§15.12).
  • Типы, представляющие типы, которые являются локальными для класса (§14.7).

Class_declaration создает новое пространство объявления (§7.3), а type_parameter и class_member_declarationсразу же содержатся в class_declaration в этом пространстве объявлений. Следующие правила применяются к class_member_declarations:

  • Конструкторы экземпляров, методы завершения и статические конструкторы должны иметь то же имя, что и немедленно заключенный класс. Все остальные члены должны иметь имена, отличающиеся от имени немедленно заключенного класса.

  • Имя параметра типа в type_parameter_list объявления класса должно отличаться от имен всех остальных параметров типа в том же type_parameter_list и должно отличаться от имени класса и имен всех членов класса.

  • Имя типа должно отличаться от имен всех членов, не относящихся к типу, объявленных в одном классе. Если два или более объявлений типов имеют одинаковое полное имя, объявления должны иметь partial модификатор (§15.2.7) и эти объявления объединяются для определения одного типа.

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

  • Имя константы, поля, свойства или события должно отличаться от имен всех остальных членов, объявленных в одном классе.

  • Имя метода должно отличаться от имен всех других не-методов, объявленных в одном классе. Кроме того, подпись (§7.6) метода должна отличаться от сигнатур всех других методов, объявленных в одном классе, и два метода, объявленные в одном классе, не должны иметь сигнатуры, которые отличаются исключительно по in, outи ref.

  • Подпись конструктора экземпляра должна отличаться от подписей всех остальных конструкторов экземпляров, объявленных в одном классе, и два конструктора, объявленные в одном классе, не должны иметь сигнатуры, которые отличаются исключительно по ref и out.

  • Подпись индексатора должна отличаться от подписей всех остальных индексаторов, объявленных в одном классе.

  • Подпись оператора должна отличаться от подписей всех остальных операторов, объявленных в одном классе.

Унаследованные члены класса (§15.3.4) не являются частью пространства объявления класса.

Примечание. Таким образом, производный класс может объявлять член с тем же именем или сигнатурой, что и унаследованный элемент (который в действительности скрывает унаследованный элемент). конечная заметка

Набор членов типа, объявленного в нескольких частях (§15.2.7), является объединением членов, объявленных в каждой части. Тела всех частей объявления типа разделяют одно и то же пространство объявления (§7.3), а область каждого элемента (§7.7) распространяется на тела всех частей. Домен специальных возможностей любого члена всегда включает все части заключенного типа; Частный член, объявленный в одной части, свободно доступен из другой части. Это ошибка во время компиляции для объявления одного элемента в нескольких части типа, если этот элемент не является типом с модификатором partial .

Пример:

partial class A
{
    int x;                   // Error, cannot declare x more than once

    partial class Inner      // Ok, Inner is a partial type
    {
        int y;
    }
}

partial class A
{
    int x;                   // Error, cannot declare x more than once

    partial class Inner      // Ok, Inner is a partial type
    {
        int z;
    }
}

пример конца

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

15.3.2 Тип экземпляра

Каждое объявление класса имеет связанный тип экземпляра. Для объявления универсального класса тип экземпляра формируется путем создания созданного типа (§8.4) из объявления типа, причем каждый из указанных аргументов типа является соответствующим параметром типа. Так как тип экземпляра использует параметры типа, его можно использовать только в том месте, где параметры типа находятся в области; то есть внутри объявления класса. Тип экземпляра — это тип кода, написанного this внутри объявления класса. Для не универсальных классов тип экземпляра — это просто объявленный класс.

Пример. Ниже показаны несколько объявлений классов, а также их типы экземпляров:

class A<T>             // instance type: A<T>
{
    class B {}         // instance type: A<T>.B
    class C<U> {}      // instance type: A<T>.C<U>
}
class D {}             // instance type: D

пример конца

15.3.3 Члены созданных типов

Ненаследуемые элементы созданного типа получаются путем подстановки для каждого type_parameter в объявлении члена, соответствующего type_argument созданного типа. Процесс подстановки основан на семантическом значении объявлений типов и не просто подстановки текста.

Пример. Учитывая объявление универсального класса

class Gen<T,U>
{
    public T[,] a;
    public void G(int i, T t, Gen<U,T> gt) {...}
    public U Prop { get {...} set {...} }
    public int H(double d) {...}
}

Созданный тип Gen<int[],IComparable<string>> имеет следующие элементы:

public int[,][] a;
public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...}
public IComparable<string> Prop { get {...} set {...} }
public int H(double d) {...}

Тип элемента в объявлении a универсального класса — "двухмерный массивGen", поэтому тип элемента T в созданном выше типе — "двухмерный массив одномерного массиваa", или int.int[,][]

пример конца

В членах функции экземпляра тип экземпляра this — это тип экземпляра (§15.3.2) содержащего объявления.

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

Пример:

class C<V>
{
    public V f1;
    public C<V> f2;

    public C(V x)
    {
        this.f1 = x;
        this.f2 = this;
    }
}

class Application
{
    static void Main()
    {
        C<int> x1 = new C<int>(1);
        Console.WriteLine(x1.f1);              // Prints 1

        C<double> x2 = new C<double>(3.1415);
        Console.WriteLine(x2.f1);              // Prints 3.1415
    }
}

пример конца

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

Класс наследует члены своего прямого базового класса. Наследование означает, что класс неявно содержит все члены своего прямого базового класса, за исключением конструкторов экземпляров, завершения и статических конструкторов базового класса. Ниже приведены некоторые важные аспекты наследования:

  • Наследование является транзитивным. Если C он является производным от B, и B является производным A, то C наследует члены, объявленные в B , а также члены, объявленные в A.

  • Производный класс расширяет его прямой базовый класс. Производный класс может дополнить наследуемые элементы новыми элементами, но он не может удалить определение для наследуемого члена.

  • Конструкторы экземпляров, методы завершения и статические конструкторы не наследуются, но все остальные члены являются независимо от объявленной специальных возможностей (§7.5). Однако в зависимости от объявленной доступности унаследованные члены могут быть недоступны в производном классе.

  • Производный класс может скрыть (§7.7.2.3), наследуя элементы, объявляя новые члены с тем же именем или подписью. Однако скрытие унаследованного элемента не удаляет этот элемент— он просто делает этот элемент недоступным непосредственно через производный класс.

  • Экземпляр класса содержит набор всех полей экземпляров, объявленных в классе и его базовых классах, а неявное преобразование (§10.2.8) существует из производного типа класса в любой из его типов базовых классов. Таким образом, ссылка на экземпляр определенного производного класса может рассматриваться как ссылка на экземпляр любого из его базовых классов.

  • Класс может объявлять виртуальные методы, свойства, индексаторы и события, а производные классы могут переопределить реализацию этих элементов функции. Это позволяет классам проявлять полиморфное поведение, в котором действия, выполняемые вызовом члена функции, зависят от типа времени выполнения экземпляра, через который вызывается этот элемент функции.

Наследуемые элементы созданного типа класса являются членами немедленного типа базового класса (§15.2.4.2), который найден путем замены аргументов типа созданного типа для каждого вхождения соответствующих параметров типа в base_class_specification. Эти члены, в свою очередь, преобразуются путем подстановки для каждого type_parameter в объявлении члена, соответствующего type_argument base_class_specification.

Пример:

class B<U>
{
    public U F(long index) {...}
}

class D<T> : B<T[]>
{
    public T G(string s) {...}
}

В приведенном выше коде созданный тип D<int> имеет открытый intG(string s) ненаследуемый член, полученный путем замены аргумента int типа для параметра Tтипа. D<int> также имеет унаследованный член из объявления Bкласса. Этот наследуемый элемент определяется первым определением типа B<int[]>D<int> базового класса путем замены int в T спецификации B<T[]>базового класса. Затем в качестве аргумента Bint[] типа заменено U в public U F(long index), что дает унаследованный членpublic int[] F(long index).

пример конца

15.3.5 Новый модификатор

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

Унаследованный элемент M считается доступным, если M он доступен, и нет другого унаследованного элемента N, который уже скрываетM. Неявно скрытие унаследованного члена не считается ошибкой, но компилятор должен выдавать предупреждение, если объявление производного члена класса не включает модификатор new, чтобы явно указать, что производный член предназначен для скрытия базового элемента. Если одна или несколько частей частичного объявления (§15.2.7) вложенного типа включают new модификатор, предупреждение не выводится, если вложенный тип скрывает доступный наследуемый элемент.

new Если модификатор включен в объявление, которое не скрывает доступный унаследованный элемент, выдается предупреждение для этого эффекта.

Модификаторы доступа 15.3.6

Class_member_declaration может иметь любой из разрешенных видов объявленных специальных возможностей (§7.5.2): public, , protected internal, protected, private protectedinternalили private. protected internal За исключением сочетаний, private protected это ошибка во время компиляции для указания нескольких модификаторов доступа. Если class_member_declaration не включает модификаторы доступа, private предполагается.

15.3.7 Составные типы

Типы, используемые в объявлении элемента, называются составляющими типами этого элемента. Возможные типы компонентов — это тип константы, поля, свойства, события или индексатора, возвращаемого типа метода или оператора, а также типы параметров метода, индексатора, оператора или конструктора экземпляра. Составные типы элемента должны быть по крайней мере так же доступны, как и сам член (§7.5.5).

15.3.8 Статические и элементы экземпляра

Члены класса — статические члены или члены экземпляра.

Примечание. Как правило, полезно рассматривать статические члены как принадлежащие классам и членам экземпляра как принадлежащие объектам (экземплярам классов). конечная заметка

Если поле, метод, свойство, событие, оператор или объявление конструктора включает static модификатор, он объявляет статический элемент. Кроме того, объявление константы или типа неявно объявляет статический элемент. Статические члены имеют следующие характеристики:

  • Если статический элемент M ссылается на member_access (§12.8.7) формы E.M, E должен указывать тип, имеющий член M. Это ошибка во время компиляции для E обозначения экземпляра.
  • Статическое поле в не универсальном классе определяет ровно одно расположение хранилища. Независимо от того, сколько экземпляров не универсального класса создается, существует только одна копия статического поля. Каждый отдельный закрытый созданный тип (§8.4.3) имеет собственный набор статических полей независимо от количества экземпляров закрытого созданного типа.
  • Статический член функции (метод, свойство, событие, оператор или конструктор) не работает с определенным экземпляром, и это ошибка во время компиляции, ссылающейся на это в таком элементе функции.

Если поле, метод, свойство, событие, индексатор, конструктор или объявление завершения не включает статический модификатор, он объявляет элемент экземпляра. (Иногда элемент экземпляра называется нестатичным элементом.) Члены экземпляра имеют следующие характеристики:

  • Если элемент M экземпляра ссылается на member_access (§12.8.7) формы E.M, E должен указывать экземпляр типа, имеющего член M. Это ошибка времени привязки для E, обозначающая тип.
  • Каждый экземпляр класса содержит отдельный набор всех полей экземпляра класса.
  • Член функции экземпляра (метод, свойство, индексатор, конструктор экземпляра или метод завершения) работает над заданным экземпляром класса, и к этому экземпляру можно получить доступ как this (§12.8.14).

Пример. В следующем примере показаны правила доступа к статическим и элементам экземпляра:

class Test
{
    int x;
    static int y;
    void F()
    {
        x = 1;               // Ok, same as this.x = 1
        y = 1;               // Ok, same as Test.y = 1
    }

    static void G()
    {
        x = 1;               // Error, cannot access this.x
        y = 1;               // Ok, same as Test.y = 1
    }

    static void Main()
    {
        Test t = new Test();
        t.x = 1;       // Ok
        t.y = 1;       // Error, cannot access static member through instance
        Test.x = 1;    // Error, cannot access instance member through type
        Test.y = 1;    // Ok
    }
}

Метод F показывает, что в члене функции экземпляра можно использовать simple_name (§12.8.4) для доступа как к элементам экземпляра, так и к статическим элементам. Метод G показывает, что в статическом элементе функции это ошибка во время компиляции для доступа к члену экземпляра через simple_name. Метод Main показывает, что в member_access (§12.8.7) элементы экземпляра должны быть доступны через экземпляры, а статические члены должны быть доступны через типы.

пример конца

15.3.9 Вложенные типы

15.3.9.1 Общие

Тип, объявленный в классе или структуре, называется вложенным типом. Тип, объявленный в единице компиляции или пространстве имен, называется не вложенным типом.

Пример. В следующем примере:

class A
{
    class B
    {
        static void F()
        {
            Console.WriteLine("A.B.F");
        }
    }
}

класс B является вложенным типом, так как он объявлен в классе A, а класс A является не вложенным типом, так как он объявлен в единице компиляции.

пример конца

15.3.9.2 Полное имя

Полное имя (§7.8.3) для вложенных объявлений S.N типа, где S является полным именем объявления типа, объявленного типаN, и N является неквалифицированным именем (§7.8.2) объявления вложенного типа (включая любые generic_dimension_specifier (§12.8.18)).

15.3.9.3 Объявленная доступность

Не вложенные типы могут иметь public или объявлять специальные возможности и internalinternal объявлять специальные возможности по умолчанию. Вложенные типы также могут иметь эти формы объявленной специальных возможностей, а также одну или несколько дополнительных форм объявленной специальных возможностей в зависимости от того, является ли содержащий тип классом или структурой:

  • Вложенный тип, объявленный в классе, может иметь любой из разрешенных типов объявленных специальных возможностей и, как и другие члены private класса, по умолчанию объявленные специальные возможности.
  • Вложенный тип, объявленный в структуре, может иметь любую из трех форм объявленной специальных возможностей (publicилиinternalprivate) и, как и другие члены структуры, по умолчанию объявленные private специальные возможности.

Пример: пример

public class List
{
    // Private data structure
    private class Node
    {
        public object Data;
        public Node? Next;

        public Node(object data, Node? next)
        {
            this.Data = data;
            this.Next = next;
        }
    }

    private Node? first = null;
    private Node? last = null;

    // Public interface
    public void AddToFront(object o) {...}
    public void AddToBack(object o) {...}
    public object RemoveFromFront() {...}
    public object RemoveFromBack() {...}
    public int Count { get {...} }
}

объявляет закрытый вложенный класс Node.

пример конца

15.3.9.4 Скрытие

Вложенный тип может скрыть (§7.7.2.2) базовый элемент. Модификатор new (§15.3.5) разрешен в объявлениях вложенных типов, чтобы скрыть их можно явно.

Пример: пример

class Base
{
    public static void M()
    {
        Console.WriteLine("Base.M");
    }
}

class Derived: Base
{
    public new class M
    {
        public static void F()
        {
            Console.WriteLine("Derived.M.F");
        }
    }
}

class Test
{
    static void Main()
    {
        Derived.M.F();
    }
}

показывает вложенный класс M , который скрывает метод M , определенный в Base.

пример конца

15.3.9.5 этот доступ

Вложенный тип и его содержащий тип не имеют особых отношений относительно this_access (§12.8.14). В частности, this в вложенный тип нельзя использовать для ссылки на элементы экземпляра содержащего типа. В случаях, когда вложенный тип должен иметь доступ к элементам экземпляра своего содержащего типа, доступ можно предоставить, предоставив this экземпляр содержащего типа в качестве аргумента конструктора для вложенного типа.

Пример: следующий пример

class C
{
    int i = 123;
    public void F()
    {
        Nested n = new Nested(this);
        n.G();
    }

    public class Nested
    {
        C this_c;

        public Nested(C c)
        {
            this_c = c;
        }

        public void G()
        {
            Console.WriteLine(this_c.i);
        }
    }
}

class Test
{
    static void Main()
    {
        C c = new C();
        c.F();
    }
}

показывает этот метод. Экземпляр C создает экземпляр Nestedи передает его Nestedконструктору, чтобы обеспечить последующий доступ к Cчленам экземпляра.

пример конца

15.3.9.6 Доступ к частным и защищенным членам содержащего типа

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

Пример: пример

class C
{
    private static void F() => Console.WriteLine("C.F");

    public class Nested
    {
        public static void G() => F();
    }
}

class Test
{
    static void Main() => C.Nested.G();
}

показывает класс C , содержащий вложенный класс Nested. В Nestedпределах метода G вызывается статический метод F , определенный в C, и F имеется частная объявленная доступность.

пример конца

Вложенный тип также может получить доступ к защищенным элементам, определенным в базовом типе его содержащего типа.

Пример. В следующем коде

class Base
{
    protected void F() => Console.WriteLine("Base.F");
}

class Derived: Base
{
    public class Nested
    {
        public void G()
        {
            Derived d = new Derived();
            d.F(); // ok
        }
    }
}

class Test
{
    static void Main()
    {
        Derived.Nested n = new Derived.Nested();
        n.G();
    }
}

Вложенный класс Derived.Nested обращается к защищенному методу F , определенному в Derivedбазовом классе, Baseпутем вызова через экземпляр Derived.

пример конца

15.3.9.7 Вложенные типы в универсальных классах

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

Каждое объявление типа, содержащееся в объявлении универсального класса, неявно является объявлением универсального типа. При написании ссылки на тип, вложенный в универсальный тип, должен быть назван содержащий созданный тип, включая его аргументы типа. Однако из внешнего класса вложенный тип может использоваться без квалификации; Тип экземпляра внешнего класса может неявно использоваться при создании вложенного типа.

Пример. В следующем примере показаны три различных правильных способа ссылки на созданный тип, из Innerкоторых первые два эквивалентны:

class Outer<T>
{
    class Inner<U>
    {
        public static void F(T t, U u) {...}
    }

    static void F(T t)
    {
        Outer<T>.Inner<string>.F(t, "abc");    // These two statements have
        Inner<string>.F(t, "abc");             // the same effect
        Outer<int>.Inner<string>.F(3, "abc");  // This type is different
        Outer.Inner<string>.F(t, "abc");       // Error, Outer needs type arg
    }
}

пример конца

Хотя это плохой стиль программирования, параметр типа в вложенном типе может скрыть элемент или параметр типа, объявленный во внешнем типе.

Пример:

class Outer<T>
{
    class Inner<T>                                  // Valid, hides Outer's T
    {
        public T t;                                 // Refers to Inner's T
    }
}

пример конца

15.3.10 Зарезервированные имена членов

15.3.10.1 General

Чтобы упростить базовую реализацию времени выполнения C#, для каждого объявления исходного члена, являющегося свойством, событием или индексатором, реализация должна зарезервировать две подписи метода на основе типа объявления члена, его имени и его типа (§15.3.10.2, §15.3.10.3, §15.3.10.4). Это ошибка во время компиляции программы для объявления члена, подпись которого соответствует подписи, зарезервированной членом, объявленным в той же области, даже если базовая реализация во время выполнения не использует эти резервирования.

Зарезервированные имена не вводят объявления, поэтому они не участвуют в поиске членов. Однако связанные с объявлением сигнатуры зарезервированных методов участвуют в наследовании (§15.3.4new§15.3.5).

Примечание. Резервирование этих имен служит трем целям:

  1. Чтобы разрешить базовой реализации использовать обычный идентификатор в качестве имени метода для получения или задания доступа к функции языка C#.
  2. Чтобы разрешить другим языкам взаимодействовать с использованием обычного идентификатора в качестве имени метода для получения или задания доступа к функции языка C#.
  3. Чтобы убедиться, что источник, принятый одним соответствующим компилятором, принимается другим, делая особенности зарезервированных имен элементов согласованными во всех реализациях C#.

конечная заметка

Объявление средства завершения (§15.13) также приводит к зарезервированию подписи (§15.3.10.5).

Некоторые имена зарезервированы для использования в качестве имен методов оператора (§15.3.10.6).

15.3.10.2 Имена членов, зарезервированные для свойств

Для свойства Pтипа (T) зарезервированы следующие подписи:

T get_P();
void set_P(T value);

Обе подписи зарезервированы, даже если свойство доступно только для чтения или записи.

Пример. В следующем коде

class A
{
    public int P
    {
        get => 123;
    }
}

class B : A
{
    public new int get_P() => 456;

    public new void set_P(int value)
    {
    }
}

class Test
{
    static void Main()
    {
        B b = new B();
        A a = b;
        Console.WriteLine(a.P);
        Console.WriteLine(b.P);
        Console.WriteLine(b.get_P());
    }
}

Класс A определяет свойство Pтолько для чтения, поэтому резервирует подписи для get_P и set_P методов. A класс B является производным от A обоих зарезервированных подписей и скрывает их. Пример создает выходные данные:

123
123
456

пример конца

15.3.10.3 Имена участников, зарезервированные для событий

Для события E (§15.8) типа Tделегата зарезервированы следующие подписи:

void add_E(T handler);
void remove_E(T handler);

15.3.10.4 Имена членов, зарезервированные для индексаторов

Для индексатора (§15.9) типа T с списком Lпараметров зарезервированы следующие подписи:

T get_Item(L);
void set_Item(L, T value);

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

Кроме того, имя Item члена зарезервировано.

Имена членов 15.3.10.5, зарезервированные для методов завершения

Для класса, содержащего метод завершения (§15.13), резервируется следующая подпись:

void Finalize();

Имена методов 15.3.10.6, зарезервированные для операторов

Следующие имена методов зарезервированы. Хотя многие имеют соответствующие операторы в этой спецификации, некоторые из них зарезервированы для использования в будущих версиях, а некоторые зарезервированы для взаимодействия с другими языками.

Имя метода Оператор C#
op_Addition + (двоичный)
op_AdditionAssignment (зарезервировано)
op_AddressOf (зарезервировано)
op_Assign (зарезервировано)
op_BitwiseAnd & (двоичный)
op_BitwiseAndAssignment (зарезервировано)
op_BitwiseOr \|
op_BitwiseOrAssignment (зарезервировано)
op_CheckedAddition (зарезервировано для дальнейшего использования)
op_CheckedDecrement (зарезервировано для дальнейшего использования)
op_CheckedDivision (зарезервировано для дальнейшего использования)
op_CheckedExplicit (зарезервировано для дальнейшего использования)
op_CheckedIncrement (зарезервировано для дальнейшего использования)
op_CheckedMultiply (зарезервировано для дальнейшего использования)
op_CheckedSubtraction (зарезервировано для дальнейшего использования)
op_CheckedUnaryNegation (зарезервировано для дальнейшего использования)
op_Comma (зарезервировано)
op_Decrement -- (префикс и постфикс)
op_Division /
op_DivisionAssignment (зарезервировано)
op_Equality ==
op_ExclusiveOr ^
op_ExclusiveOrAssignment (зарезервировано)
op_Explicit явное (сужение) приведение
op_False false
op_GreaterThan >
op_GreaterThanOrEqual >=
op_Implicit неявное (расширение) приведения
op_Increment ++ (префикс и постфикс)
op_Inequality !=
op_LeftShift <<
op_LeftShiftAssignment (зарезервировано)
op_LessThan <
op_LessThanOrEqual <=
op_LogicalAnd (зарезервировано)
op_LogicalNot !
op_LogicalOr (зарезервировано)
op_MemberSelection (зарезервировано)
op_Modulus %
op_ModulusAssignment (зарезервировано)
op_MultiplicationAssignment (зарезервировано)
op_Multiply * (двоичный)
op_OnesComplement ~
op_PointerDereference (зарезервировано)
op_PointerToMemberSelection (зарезервировано)
op_RightShift >>
op_RightShiftAssignment (зарезервировано)
op_SignedRightShift (зарезервировано)
op_Subtraction - (двоичный)
op_SubtractionAssignment (зарезервировано)
op_True true
op_UnaryNegation - (унарный)
op_UnaryPlus + (унарный)
op_UnsignedRightShift (зарезервировано для дальнейшего использования)
op_UnsignedRightShiftAssignment (зарезервировано)

Константы 15.4

Константа — это член класса, представляющий постоянное значение: значение, которое можно вычислить во время компиляции. Constant_declaration вводит одну или несколько констант заданного типа.

constant_declaration
    : attributes? constant_modifier* 'const' type constant_declarators ';'
    ;

constant_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    ;

Constant_declaration может включать набор атрибутов(§22new) и любой из разрешенных видов объявленной доступности (§15.3.6). Атрибуты и модификаторы применяются ко всем элементам, объявленным constant_declaration. Несмотря на то что константы считаются статическими элементами, constant_declaration не требует и не разрешает static модификатор. Это ошибка для одного модификатора несколько раз в объявлении константы.

Тип constant_declaration указывает тип элементов, введенных объявлением. За типом следует список constant_declarator s (§13.6.3), каждый из которыхпредставляет новый элемент. Constant_declarator состоит из идентификатора, который называет элемент, а затем маркером "=" и constant_expression (§12.23), который дает значение элемента.

Тип, указанный в объявлении константы, должен быть , sbytestring или reference_type. Каждый constant_expression должен получить значение целевого типа или типа, который можно преобразовать в целевой тип путем неявного преобразования (§10.2).

Тип константы должен быть по крайней мере доступен как сам константа (§7.5.5).

Значение константы получается в выражении с помощью simple_name (§12.8.4) или member_access (§12.8.7).

Константа может участвовать в constant_expression. Таким образом, константу можно использовать в любой конструкции, требующей constant_expression.

Примечание. Примеры таких конструкций включают case метки, операторы, goto caseenum объявления членов, атрибуты и другие объявления констант. конечная заметка

Примечание. Как описано в §12.23, constant_expression является выражением, которое можно полностью оценить во время компиляции. Так как единственным способом создания ненулевого значения reference_type, отличного string от применения new оператора, и поскольку new оператор не допускается в constant_expression, единственное возможное значение для констант reference_type, отличных от того, что string естьnull. конечная заметка

Если нужно символьное имя для константного значения, но если тип этого значения не разрешен в объявлении константы, или если значение невозможно вычислить во время компиляции constant_expression, можно использовать поле чтения (§15.5.3).

Примечание. Семантика управления версиями const и readonly отличается (§15.5.3.3). конечная заметка

Объявление константы, объявляющее несколько констант, эквивалентно нескольким объявлениям отдельных констант с одинаковыми атрибутами, модификаторами и типом.

Пример:

class A
{
    public const double X = 1.0, Y = 2.0, Z = 3.0;
}

эквивалентно правилу

class A
{
    public const double X = 1.0;
    public const double Y = 2.0;
    public const double Z = 3.0;
}

пример конца

Константы могут зависеть от других констант в той же программе, если зависимости не имеют циклического характера.

Пример. В следующем коде

class A
{
    public const int X = B.Z + 1;
    public const int Y = 10;
}

class B
{
    public const int Z = A.Y + 1;
}

Компилятор должен сначала оценить A.Y, а затем оценить B.Zи, наконец, оценить A.X, создать значения 10, 11и 12.

пример конца

Объявления констант могут зависеть от констант других программ, но такие зависимости возможны только в одном направлении.

Пример. Если и A были объявлены в отдельных программахB, A.X можно было бы зависеть от этого, но B.Z тогда не может одновременно зависеть B.Zот A.Yнего. пример конца

Поля 15.5

15.5.1 Общие

Поле — это элемент, представляющий переменную, связанную с объектом или классом. В field_declaration представлено одно или несколько полей заданного типа.

field_declaration
    : attributes? field_modifier* type variable_declarators ';'
    ;

field_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'readonly'
    | 'volatile'
    | unsafe_modifier   // unsafe code support
    ;

variable_declarators
    : variable_declarator (',' variable_declarator)*
    ;

variable_declarator
    : identifier ('=' variable_initializer)?
    ;

unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).

Field_declaration может включать набор атрибутов (§22), модификатор (new), допустимое сочетание четырех модификаторов доступа (§15.3.6) и static модификатор (§15.5.2). Кроме того, field_declaration может включать модификатор (§15.5.3volatile), но не оба. Атрибуты и модификаторы применяются ко всем элементам, объявленным field_declaration. Это ошибка для одного модификатора несколько раз в field_declaration.

Тип field_declaration указывает тип элементов, введенных объявлением. За типом следует список variable_declarators, каждый из которых представляет новый член. Variable_declarator состоит из идентификатора, именующего этот член, а также маркера "=" и variable_initializer (§15.5.6), который дает начальное значение этого элемента.

Тип поля должен быть по крайней мере таким же доступным, как сам поле (§7.5.5).

Значение поля получается в выражении с помощью simple_name (§12.8.4), member_access (§12.8.7) или base_access (§12.8.15). Значение поля без чтения изменяется с помощью назначения (§12.21). Значение нечитаемого поля может быть получено и изменено с помощью операторов добавочного и декремента постфикса (§12.8.16) и префиксов и добавочных и декрементных операторов (§12.9.6).

Объявление поля, объявляющее несколько полей, эквивалентно нескольким объявлениям отдельных полей с одинаковыми атрибутами, модификаторами и типом.

Пример:

class A
{
    public static int X = 1, Y, Z = 100;
}

эквивалентно правилу

class A
{
    public static int X = 1;
    public static int Y;
    public static int Z = 100;
}

пример конца

Поля статического и экземпляра 15.5.2

Если объявление поля включает static модификатор, поля, представленные объявлением, являются статическими полями. Если модификатор отсутствует static , поля, представленные объявлением, являются полями экземпляра. Статические поля и поля экземпляров являются двумя из нескольких типов переменных (§9), поддерживаемых C#, и иногда они называются статическими переменными и переменными экземпляра соответственно.

Как описано в §15.3.8, каждый экземпляр класса содержит полный набор полей экземпляра класса, в то время как существует только один набор статических полей для каждого не универсального класса или закрытого типа, независимо от количества экземпляров класса или закрытого созданного типа.

Поля чтения 15.5.3

15.5.3.1 Общие

Если field_declaration включает readonly модификатор, поля, представленные объявлением, являются полями считывания. Прямые назначения для полей чтения могут возникать только в рамках этого объявления или в конструкторе экземпляра или статического конструктора в том же классе. (Поле чтения можно назначить несколько раз в этих контекстах.) В частности, прямые назначения в поле чтения разрешены только в следующих контекстах:

  • В variable_declarator, в которой представлено поле (включая variable_initializer в объявлении).
  • Для поля экземпляра в конструкторах экземпляра класса, содержащего объявление поля; для статического поля в статическом конструкторе класса, содержащего объявление поля. Это также единственные контексты, в которых допустимо передать поле чтения в качестве выходного или ссылочного параметра.

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

15.5.3.2 Использование статических полей чтения для констант

Статическое поле чтения полезно при необходимости символьного имени для константного значения, но если тип значения не допускается в объявлении констант или когда значение невозможно вычислить во время компиляции.

Пример. В следующем коде

public class Color
{
    public static readonly Color Black = new Color(0, 0, 0);
    public static readonly Color White = new Color(255, 255, 255);
    public static readonly Color Red = new Color(255, 0, 0);
    public static readonly Color Green = new Color(0, 255, 0);
    public static readonly Color Blue = new Color(0, 0, 255);

    private byte red, green, blue;

    public Color(byte r, byte g, byte b)
    {
        red = r;
        green = g;
        blue = b;
    }
}

Black, , WhiteRedGreenи Blue члены не могут быть объявлены как константные члены, так как их значения не могут быть вычисляться во время компиляции. Однако объявление их static readonly вместо этого имеет гораздо тот же эффект.

пример конца

15.5.3.3 Управление версиями констант и статических полей чтения

Константы и поля чтения имеют другую семантику двоичного управления версиями. Когда выражение ссылается на константу, значение константы получается во время компиляции, но когда выражение ссылается на поле чтения, значение поля не получается до времени выполнения.

Пример. Рассмотрим приложение, состоящее из двух отдельных программ:

namespace Program1
{
    public class Utils
    {
        public static readonly int x = 1;
    }
}

и

namespace Program2
{
    class Test
    {
        static void Main()
        {
            Console.WriteLine(Program1.Utils.X);
        }
    }
}

Program1 Пространства Program2 имен указывают две программы, скомпилированные отдельно. Так как Program1.Utils.X он объявлен как static readonly поле, выходные данные Console.WriteLine инструкции не известны во время компиляции, а получаются во время выполнения. Таким образом, если значение изменено X и Program1 перекомпилируется, Console.WriteLine инструкция выводит новое значение, даже если Program2 не перекомпилируется. Тем не менее, было X константой, значение X было бы получено во время Program2 компиляции, и будет оставаться не затронутым изменениями до Program1 повторной Program2 компиляции.

пример конца

15.5.4 Переменные поля

Если field_declaration включает volatile модификатор, поля, представленные этим объявлением, являются переменными полями. Для ненезависимых полей методы оптимизации, которые могут изменить порядок инструкций, могут привести к непредвиденным и непредсказуемым результатам в многопоточных программах, которые обращаются к полям без синхронизации, например, которые предоставляются lock_statement (§13.13). Эти оптимизации могут выполняться компилятором, системой выполнения или оборудованием. Для переменных полей такие оптимизации переупорядочения ограничены:

  • Чтение изменяющегося поля называется переменным чтением. Переменная чтение имеет "получение семантики"; т. е. это гарантируется до любых ссылок на память, которая возникает после нее в последовательности инструкций.
  • Запись изменяющегося поля называется переменной записью. Переменная запись имеет "семантику выпуска"; То есть это гарантируется после всех ссылок на память до инструкции записи в последовательности инструкций.

Эти ограничения гарантируют, что все потоки будут видеть временные записи, выполняемые другим потоком, в порядке выполнения. Соответствующая реализация не требуется для предоставления единого общего порядка переменных записей, как показано во всех потоках выполнения. Тип изменяющегося поля должен быть одним из следующих:

  • Reference_type.
  • Type_parameter, который, как известно, является ссылочным типом (§15.2.5).
  • Типbyte, sbyte, shortushortintuintcharfloatboolSystem.IntPtrили .System.UIntPtr
  • Enum_type с типом byte , sbyte, , shortushortintили uint.

Пример: пример

class Test
{
    public static int result;
    public static volatile bool finished;

    static void Thread2()
    {
        result = 143;
        finished = true;
    }

    static void Main()
    {
        finished = false;

        // Run Thread2() in a new thread
        new Thread(new ThreadStart(Thread2)).Start();    

        // Wait for Thread2() to signal that it has a result
        // by setting finished to true.
        for (;;)
        {
            if (finished)
            {
                Console.WriteLine($"result = {result}");
                return;
            }
        }
    }
}

выводятся следующие выходные данные:

result = 143

В этом примере метод Main запускает новый поток, который запускает метод Thread2. Этот метод сохраняет значение в ненезависимом поле, которое вызывается result, а затем сохраняется true в переменном поле finished. Основной поток ожидает задания finishedполяtrue, а затем считывает полеresult. С момента finished объявления volatileосновной поток должен считывать значение 143 из поля result. Если поле finished не было объявленоvolatile, то будет допустимо, чтобы result хранилище было видимым для основного потока послеfinishedсчитывать значение 0 из поляresult. finished Объявление как volatile поле предотвращает такое несоответствие.

пример конца

Инициализация поля 15.5.5

Начальное значение поля, будь то статическое поле или поле экземпляра, является значением по умолчанию (§9.3) типа поля. Невозможно наблюдать за значением поля до того, как эта инициализация по умолчанию произошла, и поле таким образом никогда не является "неинициализированным".

Пример: пример

class Test
{
    static bool b;
    int i;

    static void Main()
    {
        Test t = new Test();
        Console.WriteLine($"b = {b}, i = {t.i}");
    }
}

выводятся следующие выходные данные

b = False, i = 0

так как b и оба автоматически инициализированы i для значений по умолчанию.

пример конца

Инициализаторы переменных 15.5.6

15.5.6.1 Общие

Объявления полей могут включать variable_initializers. Для статических полей инициализаторы переменных соответствуют операторам присваивания, выполняемым во время инициализации класса. Например, поля инициализаторы переменных соответствуют операторам присваивания, выполняемым при создании экземпляра класса.

Пример: пример

class Test
{
    static double x = Math.Sqrt(2.0);
    int i = 100;
    string s = "Hello";

    static void Main()
    {
        Test a = new Test();
        Console.WriteLine($"x = {x}, i = {a.i}, s = {a.s}");
    }
}

выводятся следующие выходные данные

x = 1.4142135623730951, i = 100, s = Hello

Поскольку назначение x происходит, когда инициализаторы i статических полей выполняются и присваиваются и s возникают при выполнении инициализаторов полей экземпляра.

пример конца

Инициализация значений по умолчанию, описанная в §15.5.5 , возникает для всех полей, включая поля с инициализаторами переменных. Таким образом, при инициализации класса все статические поля в этом классе сначала инициализированы в значения по умолчанию, а затем инициализаторы статических полей выполняются в текстовом порядке. Аналогичным образом, когда создается экземпляр класса, все поля экземпляров в этом экземпляре сначала инициализированы в значениях по умолчанию, а затем инициализаторы полей экземпляра выполняются в текстовом порядке. При наличии объявлений полей в нескольких объявлениях частичного типа для одного типа порядок частей не определен. Однако в каждой части инициализаторы полей выполняются в порядке.

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

Пример: Однако это настоятельно не рекомендуется в стиле. Пример

class Test
{
    static int a = b + 1;
    static int b = a + 1;

    static void Main()
    {
        Console.WriteLine($"a = {a}, b = {b}");
    }
}

демонстрирует это поведение. Несмотря на циклические a определения и b, программа действительна. В результате выходные данные

a = 1, b = 2

поскольку статические поля a и инициализированы b0 (значение по умолчанию для int) перед выполнением инициализаторов. При выполнении инициализатора a значение b равно нулю и поэтому a инициализируется 1в . Когда инициализатор для b выполнения, значение объекта уже 1инициализировано и поэтому инициализировано b в 2.

пример конца

Инициализация статического поля 15.5.6.2

Инициализаторы статических полей класса соответствуют последовательности назначений, выполняемых в текстовом порядке, в котором они отображаются в объявлении класса (§15.5.6.1). В частичном классе значение "текстового порядка" указывается в §15.5.6.1. Если статический конструктор (§15.12) существует в классе, выполнение инициализаторов статических полей происходит непосредственно перед выполнением статического конструктора. В противном случае инициализаторы статических полей выполняются во время, зависящее от реализации, до первого использования статического поля этого класса.

Пример: пример

class Test
{
    static void Main()
    {
        Console.WriteLine($"{B.Y} {A.X}");
    }

    public static int F(string s)
    {
        Console.WriteLine(s);
        return 1;
    }
}

class A
{
    public static int X = Test.F("Init A");
}

class B
{
    public static int Y = Test.F("Init B");
}

Может вывести либо выходные данные:

Init A
Init B
1 1

или выходные данные:

Init B
Init A
1 1

Поскольку выполнение инициализатора и инициализатора Xинициализатора Yможет происходить в любом порядке; они могут возникать только до ссылок на эти поля. Однако в примере:

class Test
{
    static void Main()
    {
        Console.WriteLine($"{B.Y} {A.X}");
    }

    public static int F(string s)
    {
        Console.WriteLine(s);
        return 1;
    }
}

class A
{
    static A() {}
    public static int X = Test.F("Init A");
}

class B
{
    static B() {}
    public static int Y = Test.F("Init B");
}

Выходные данные должны быть:

Init B
Init A
1 1

поскольку правила выполнения статических конструкторов (как определено в §15.12) предоставляют, что статический конструктор (и, следовательноB, Bинициализаторы статических полей) должен выполняться перед Aстатическим конструктором и инициализаторами полей.

пример конца

Инициализация поля экземпляра 15.5.6.3

Инициализаторы переменных поля экземпляра класса соответствуют последовательности назначений, которые выполняются сразу после входа в любой из конструкторов экземпляра (§15.11.3) этого класса. В частичном классе значение "текстового порядка" указывается в §15.5.6.1. Инициализаторы переменных выполняются в текстовом порядке, в котором они отображаются в объявлении класса (§15.5.6.1). Процесс создания и инициализации экземпляра класса описан далее в разделе §15.11.

Инициализатор переменных для поля экземпляра не может ссылаться на созданный экземпляр. Таким образом, это ошибка во время компиляции для ссылки this на инициализатор переменной, так как это ошибка во время компиляции для инициализатора переменной для ссылки на любой член экземпляра через simple_name.

Пример. В следующем коде

class A
{
    int x = 1;
    int y = x + 1;     // Error, reference to instance member of this
}

инициализатор переменных для y результатов ошибки во время компиляции, так как ссылается на элемент создаваемого экземпляра.

пример конца

Методы 15.6

15.6.1 Общие

Метод — это член, реализующий вычисление или действие, которое может выполнять объект или класс. Методы объявляются с помощью method_declaration:

method_declaration
    : attributes? method_modifiers return_type method_header method_body
    | attributes? ref_method_modifiers ref_kind ref_return_type method_header
      ref_method_body
    ;

method_modifiers
    : method_modifier* 'partial'?
    ;

ref_kind
    : 'ref'
    | 'ref' 'readonly'
    ;

ref_method_modifiers
    : ref_method_modifier*
    ;

method_header
    : member_name '(' parameter_list? ')'
    | member_name type_parameter_list '(' parameter_list? ')'
      type_parameter_constraints_clause*
    ;

method_modifier
    : ref_method_modifier
    | 'async'
    ;

ref_method_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

return_type
    : ref_return_type
    | 'void'
    ;

ref_return_type
    : type
    ;

member_name
    : identifier
    | interface_type '.' identifier
    ;

method_body
    : block
    | '=>' null_conditional_invocation_expression ';'
    | '=>' expression ';'
    | ';'
    ;

ref_method_body
    : block
    | '=>' 'ref' variable_reference ';'
    | ';'
    ;

Заметки грамматики:

Примечание. Перекрытие и приоритет между альтернативами здесь исключительно для описательного удобства; правила грамматики можно разработать, чтобы удалить перекрытие. ANTLR и другие системы грамматики принимают то же удобство и поэтому method_body автоматически имеет указанную семантику. конечная заметка

Method_declaration может включать набор атрибутов (§22) и один из разрешенных видов объявленной доступности (§15.3.6), (new), (§15.6.3), (§15.3), (virtual), (override), (§15.6.6sealed§15.6.7), abstract (extern) и (async) модификаторов.

Объявление имеет допустимое сочетание модификаторов, если все из следующих значений имеют значение true:

  • Объявление включает допустимое сочетание модификаторов доступа (§15.3.6).
  • Объявление не включает один и тот же модификатор несколько раз.
  • Объявление включает в себя не более одного из следующих модификаторов: static, virtualи override.
  • Объявление включает в себя не более одного из следующих модификаторов: new и override.
  • Если объявление включает abstract модификатор, объявление не содержит ни одного из следующих модификаторов: static, , virtualsealedили extern.
  • Если объявление включает private модификатор, объявление не содержит ни одного из следующих модификаторов: virtual, overrideили abstract.
  • Если объявление включает sealed модификатор, то объявление также включает override модификатор.
  • Если объявление включает модификатор, он не включает partial ни один из следующих модификаторов: new, publicprotectedinternalprivatevirtualsealedoverrideabstractили .extern

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

  • Если ref он присутствует, метод возвращается по ссылке и возвращает ссылку на переменную, которая при необходимости доступна только для чтения;
  • В противном случае, если return_type , voidметод возвращает значение без значения и не возвращает значение;
  • В противном случае метод возвращается по значению и возвращает значение.

Return_type объявления метода return-by-value или return-no-value указывает тип результата, если таковой имеется, возвращаемый методом. Только метод возвращаемого значения может включать partial модификатор (§15.6.9). Если объявление включает модификатор, async должен быть или метод возвращает по значению, а возвращаемый тип — void задачи (§15.15.1).

Ref_return_type объявления метода return-by-ref указывает тип переменной, на которую ссылается variable_reference, возвращаемой методом.

Универсальный метод — это метод, объявление которого включает type_parameter_list. Это указывает параметры типа для метода. Необязательные type_parameter_constraints_clauseуказывают ограничения для параметров типа.

Универсальный method_declaration для явной реализации элемента интерфейса не должен иметь никаких type_parameter_constraints_clause; объявление наследует какие-либо ограничения от ограничений метода интерфейса.

Аналогичным образом объявление метода с override модификатором не должно иметь никаких type_parameter_constraints_clause, а ограничения параметров типа метода наследуются от переопределенного виртуального метода.

Member_name указывает имя метода. Если метод не является явной реализацией члена интерфейса (§18.6.2), member_name является просто идентификатором.

Для явной реализации элемента интерфейса member_name состоит из interface_type, за которым следует "." и идентификатор. В этом случае объявление не должно содержать модификаторов, отличных от (возможно) extern или async.

Необязательный parameter_list задает параметры метода (§15.6.2).

Return_type или ref_return_type, а каждый из типов, на которые ссылается parameter_listметода, должен быть по крайней мере доступен как сам метод (§7.5.5).

Method_body метода возвращаемого по значению или возвращаемого значения является точкой с запятой, телом блока или телом выражения. Блок состоит из блока, который указывает инструкции, выполняемые при вызове метода. Текст выражения состоит из =>null_conditional_invocation_expression или выражения, а также с запятой и обозначает одно выражение для выполнения при вызове метода.

Для абстрактных и экстерн-методов method_body состоит просто из точки с запятой. Для частичных методов method_body может состоять из точки с запятой, блочного тела или тела выражения. Для всех других методов method_body является блокным телом или телом выражения.

Если method_body состоит из точки с запятой, объявление не должно включать async модификатор.

Ref_method_body метода возвращаемого по ссылке является точкой с запятой, телом блока или телом выражения. Блок состоит из блока, который указывает инструкции, выполняемые при вызове метода. Текст выражения состоит из =>, за которым следует refvariable_reference и точка с запятой, и обозначает один variable_reference для оценки при вызове метода.

Для абстрактных и экстерн-методов ref_method_body состоит просто из точки с запятой; для всех остальных методов ref_method_body является либо блоком, либо телом выражения.

Имя, число параметров типа и список параметров метода определяют подпись (§7.6) метода. В частности, сигнатура метода состоит из его имени, числа параметров типа и числа, parameter_mode_modifier s (§15.6.2.1) итипов его параметров. Возвращаемый тип не является частью сигнатуры метода, а также имена параметров, имен параметров типа или ограничений. Если тип параметра ссылается на параметр типа метода, порядковая позиция параметра типа (а не имя параметра типа) используется для эквивалентности типов.

Имя метода должно отличаться от имен всех других не-методов, объявленных в одном классе. Кроме того, подпись метода должна отличаться от сигнатур всех других методов, объявленных в одном классе, и два метода, объявленные в одном классе, не должны иметь сигнатуры, которые отличаются исключительно по in, outи ref.

Type_parameter метода находятся в области method_declaration и могут использоваться для формирования типов в пределах этой области вreturn_type или ref_return_type, method_body или ref_method_body и type_parameter_constraints_clause, но не в атрибутах.

Все параметры и параметры типа должны иметь разные имена.

Параметры метода 15.6.2

15.6.2.1 Общие

Параметры метода, если таковые имеются, объявляются parameter_list метода.

parameter_list
    : fixed_parameters
    | fixed_parameters ',' parameter_array
    | parameter_array
    ;

fixed_parameters
    : fixed_parameter (',' fixed_parameter)*
    ;

fixed_parameter
    : attributes? parameter_modifier? type identifier default_argument?
    ;

default_argument
    : '=' expression
    ;

parameter_modifier
    : parameter_mode_modifier
    | 'this'
    ;

parameter_mode_modifier
    : 'ref'
    | 'out'
    | 'in'
    ;

parameter_array
    : attributes? 'params' array_type identifier
    ;

Список параметров состоит из одного или нескольких параметров, разделенных запятыми, из которых только последний может быть parameter_array.

Fixed_parameter состоит из необязательных наборов атрибутов (§22); необязательныйin, outrefили this модификатор; тип, идентификатор и необязательный default_argument. Каждый fixed_parameter объявляет параметр заданного типа с заданным именем. Модификатор this назначает метод в качестве метода расширения и допускается только для первого параметра статического метода в не универсальном не вложенный статический класс. Если параметр является типом или параметром struct типа, ограниченным значением, structthis модификатор может сочетаться с ref модификатором или in модификатором, но не модификаторомout. Методы расширения описаны далее в разделе §15.6.10. Fixed_parameter с default_argument называется необязательным параметром, а fixed_parameter без default_argument является обязательным параметром. Обязательный параметр не должен отображаться после необязательного параметра в parameter_list.

Параметр с модификатором или модификатором ref.outthis Входной параметр может иметь default_argument. Выражение в default_argument должно быть одним из следующих:

  • constant_expression
  • выражение формы new S() , где S является типом значения
  • выражение формы default(S) , где S является типом значения

Выражение должно быть неявно преобразовано удостоверением или преобразуемым значением NULL в тип параметра.

Если необязательные параметры происходят в объявлении частичного метода (§15.6.9), явная реализация члена интерфейса (§18.6.2), объявление индексатора с одним параметром (§3.15.9 или в объявлении оператора (§15.10.1) компилятор должен дать предупреждение, так как эти члены никогда не могут вызываться таким образом, чтобы аргументы были опущены.

Parameter_array состоит из необязательного набора атрибутов (§22), params модификатора, array_type и идентификатора. Массив параметров объявляет один параметр заданного типа массива с заданным именем. Array_type массива параметров должен быть одномерным типом массива (§17.2). В вызове метода массив параметров разрешает указывать один аргумент заданного типа массива, или позволяет указывать ноль или больше аргументов типа элемента массива. Массивы параметров описаны далее в разделе §15.6.2.4.

Parameter_array может возникать после необязательного параметра, но не может иметь значение по умолчанию — пропуск аргументов для parameter_array вместо этого приведет к созданию пустого массива.

Пример: ниже показаны различные виды параметров:

void M<T>(
    ref int i,
    decimal d,
    bool b = false,
    bool? n = false,
    string s = "Hello",
    object o = null,
    T t = default(T),
    params int[] a
) { }

В parameter_list для M, i является обязательным параметром, ref является обязательным d параметром значения, bи sot являются необязательными параметрами значения и a является массивом параметров.

пример конца

Объявление метода создает отдельное пространство объявления (§7.3) для параметров и параметров типа. Имена вводятся в это пространство объявления по списку параметров типа и списку параметров метода. Текст метода, если таковой имеется, считается вложенным в это пространство объявления. Это ошибка для двух членов пространства объявления метода с одинаковым именем.

Вызов метода (§12.8.10.2) создает копию, конкретную для этого вызова, параметры и локальные переменные метода, а список аргументов вызова присваивает значения или переменные ссылки на только что созданные параметры. В блокеметода параметры можно ссылаться по их идентификаторам в выражениях simple_name (§12.8.4).

Существуют следующие виды параметров:

Примечание. Как описано в §7.6, inoutмодификаторы и ref модификаторы являются частью сигнатуры метода, но params модификатор не является. конечная заметка

Параметры значения 15.6.2.2

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

Правила определенного назначения см. в разделе "9.2.5".

Соответствующий аргумент в вызове метода должен быть выражением, которое неявно преобразуется (§10.2) в тип параметра.

Метод может назначать новые значения параметру значения. Такие назначения влияют только на расположение локального хранилища, представленное параметром значения. Они не влияют на фактический аргумент, заданный в вызове метода.

Параметры 15.6.2.3 по ссылке

15.6.2.3.1 Общие

Входные, выходные и ссылочные параметры — это параметры по ссылке. Параметр путем ссылки — это локальная ссылочная переменная (§9.7). Исходный референт получается из соответствующего аргумента, предоставленного в вызове метода.

Примечание. Ссылочный параметр можно изменить с помощью оператора назначения ссылок (= ref).

Если параметр является параметром по ссылке, соответствующий аргумент в вызове метода должен состоять из соответствующего ключевого слова, inrefили, за outкоторым следует variable_reference (§9.5) того же типа, что и параметр. Однако, если параметр является параметром in, аргумент может быть выражением , для которого существует неявное преобразование (§10.2) из этого выражения аргумента в тип соответствующего параметра.

Ссылочные параметры не допускаются для функций, объявленных как итератор (§15.14) или асинхронной функции (§15.15).

В методе, который принимает несколько параметров по ссылке, можно представить одно и то же расположение хранилища.

Входные параметры 15.6.2.3.2

Параметр, объявленный модификаторомin, является входным параметром. Аргумент, соответствующий входной параметру, является переменной, существующей в точке вызова метода, или одной из них, созданной реализацией (§12.6.2.3) в вызове метода. Правила определенного назначения см. в разделе "9.2.8".

Это ошибка во время компиляции для изменения значения входного параметра.

Примечание. Основная цель входных параметров — это эффективность. Если тип параметра метода является большой структурой (с точки зрения требований к памяти), полезно избежать копирования всего аргумента при вызове метода. Входные параметры позволяют методам ссылаться на существующие значения в памяти, обеспечивая защиту от нежелательных изменений этих значений. конечная заметка

Параметры ссылки 15.6.2.3.3

Параметр, объявленный модификатором, является ссылочным параметромref. Правила определенного назначения см. в разделе "9.2.6".

Пример: пример

class Test
{
    static void Swap(ref int x, ref int y)
    {
        int temp = x;
        x = y;
        y = temp;
    }

    static void Main()
    {
        int i = 1, j = 2;
        Swap(ref i, ref j);
        Console.WriteLine($"i = {i}, j = {j}");
    }
}

выводятся следующие выходные данные

i = 2, j = 1

Для вызова Swap в Main, x представляет i и y представляет j. Таким образом, вызов имеет эффект переключения значений i и j.

пример конца

Пример. В следующем коде

class A
{
    string s;
    void F(ref string a, ref string b)
    {
        s = "One";
        a = "Two";
        b = "Three";
    }

    void G()
    {
        F(ref s, ref s);
    }
}

Вызов invocation FG передает ссылку s на оба a и b. Таким образом, для этого вызова имена saи все относятся к одному расположению хранилища, а b три назначения изменяют поле sэкземпляра.

пример конца

struct Для типа в методе экземпляра метод доступа экземпляра (§12.2.1this§12.8.14).

Параметры вывода 15.6.2.3.4

Параметр, объявленный модификатором, является выходным out параметром. Правила определенного назначения см. в разделе "9.2.7".

Метод, объявленный как частичный метод (§15.6.9), не должен иметь выходных параметров.

Примечание. Выходные параметры обычно используются в методах, которые создают несколько возвращаемых значений. конечная заметка

Пример:

class Test
{
    static void SplitPath(string path, out string dir, out string name)
    {
        int i = path.Length;
        while (i > 0)
        {
            char ch = path[i - 1];
            if (ch == '\\' || ch == '/' || ch == ':')
            {
                break;
            }
            i--;
        }
        dir = path.Substring(0, i);
        name = path.Substring(i);
    }

    static void Main()
    {
        string dir, name;
        SplitPath(@"c:\Windows\System\hello.txt", out dir, out name);
        Console.WriteLine(dir);
        Console.WriteLine(name);
    }
}

Пример создает выходные данные:

c:\Windows\System\
hello.txt

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

пример конца

Массивы параметров 15.6.2.4

Параметр, объявленный модификатором, является массивом params параметров. Если список параметров содержит массив параметров, он должен быть последним параметром в списке, и он должен иметь одномерный тип массива.

Пример. Типы string[] и string[][] могут использоваться в качестве типа массива параметров, но тип string[,] не может. пример конца

Примечание. Невозможно объединить params модификатор с модификаторами in, outили ref. конечная заметка

Массив параметров позволяет указывать аргументы одним из двух способов в вызове метода:

  • Аргумент, заданный для массива параметров, может быть одним выражением, которое неявно преобразуется (§10.2) в тип массива параметров. В этом случае массив параметров действует точно так же, как параметр значения.
  • Кроме того, вызов может указывать ноль или больше аргументов для массива параметров, где каждый аргумент является выражением, неявно преобразованным (§10.2) в тип элемента массива параметров. В этом случае вызов создает экземпляр типа массива параметров с длиной, соответствующей количеству аргументов, инициализирует элементы экземпляра массива с заданными значениями аргументов и использует только что созданный экземпляр массива в качестве фактического аргумента.

За исключением разрешения переменного числа аргументов в вызове, массив параметров точно эквивалентен параметру значения (§15.6.2.2) одного типа.

Пример: пример

class Test
{
    static void F(params int[] args)
    {
        Console.Write($"Array contains {args.Length} elements:");
        foreach (int i in args)
        {
            Console.Write($" {i}");
        }
        Console.WriteLine();
    }

    static void Main()
    {
        int[] arr = {1, 2, 3};
        F(arr);
        F(10, 20, 30, 40);
        F();
    }
}

выводятся следующие выходные данные

Array contains 3 elements: 1 2 3
Array contains 4 elements: 10 20 30 40
Array contains 0 elements:

Первый вызов F просто передает массив arr в качестве параметра значения. Второй вызов F автоматически создает четырехэлементный int[] элемент с заданными значениями элементов и передает этот экземпляр массива в качестве параметра значения. Аналогичным образом, третий вызов F создает нулевой элемент int[] и передает этот экземпляр в качестве параметра значения. Второй и третий вызовы точно эквивалентны написанию:

F(new int[] {10, 20, 30, 40});
F(new int[] {});

пример конца

При разрешении перегрузки метод с массивом параметров может применяться либо в обычной форме, либо в развернутой форме (§12.6.4.2). Расширенная форма метода доступна только в том случае, если обычная форма метода неприменима, и только если применимый метод с той же сигнатурой, что и развернутая форма, еще не объявлена в том же типе.

Пример: пример

class Test
{
    static void F(params object[] a) =>
        Console.WriteLine("F(object[])");

    static void F() =>
        Console.WriteLine("F()");

    static void F(object a0, object a1) =>
        Console.WriteLine("F(object,object)");

    static void Main()
    {
        F();
        F(1);
        F(1, 2);
        F(1, 2, 3);
        F(1, 2, 3, 4);
    }
}

выводятся следующие выходные данные

F()
F(object[])
F(object,object)
F(object[])
F(object[])

В примере два возможных расширенных форм метода с массивом параметров уже включены в класс как обычные методы. Поэтому эти развернутые формы не учитываются при разрешении перегрузки, а вызовы первого и третьего метода таким образом выбирают обычные методы. Когда класс объявляет метод с массивом параметров, также не редко включает некоторые развернутые формы в качестве обычных методов. Таким образом, можно избежать выделения экземпляра массива, возникающего при вызове расширенной формы метода с массивом параметров.

пример конца

Массив является ссылочным типом, поэтому значение, переданное для массива параметров, может быть null.

Пример: пример:

class Test
{
    static void F(params string[] array) =>
        Console.WriteLine(array == null);

    static void Main()
    {
        F(null);
        F((string) null);
    }
}

выводятся следующие выходные данные:

True
False

Второй вызов создает False , так как он эквивалентен F(new string[] { null }) и передает массив, содержащий одну ссылку null.

пример конца

Если тип массива параметров имеет object[]значение, потенциальная неоднозначность возникает между обычной формой метода и развернутой формой для одного object параметра. Причина неоднозначности заключается в том, что object[] сам по себе неявно преобразуется в тип object. Неоднозначность не представляет никакой проблемы, так как она может быть решена путем вставки приведения при необходимости.

Пример: пример

class Test
{
    static void F(params object[] args)
    {
        foreach (object o in args)
        {
            Console.Write(o.GetType().FullName);
            Console.Write(" ");
        }
        Console.WriteLine();
    }

    static void Main()
    {
        object[] a = {1, "Hello", 123.456};
        object o = a;
        F(a);
        F((object)a);
        F(o);
        F((object[])o);
    }
}

выводятся следующие выходные данные

System.Int32 System.String System.Double
System.Object[]
System.Object[]
System.Int32 System.String System.Double

В первых и последних вызовах Fобычной формы F применимо, так как неявное преобразование существует из типа аргумента в тип параметра (оба типа ).object[] Таким образом, разрешение перегрузки выбирает обычную форму F, а аргумент передается как обычный параметр значения. Во втором и третьем вызовах обычная форма не применима, так как неявное преобразование не существует из типа аргумента F в тип параметра (тип object неявно преобразован в тип object[]). Однако расширенная форма F применима, поэтому она выбирается разрешением перегрузки. В результате один элемент object[] создается вызовом, и один элемент массива инициализируется заданным значением аргумента (само по себе является ссылкой на объект object[]).

пример конца

Методы статического и экземпляра 15.6.3

Если объявление метода включает static модификатор, этот метод, как говорят, является статическим методом. Если модификатор отсутствует static , метод считается методом экземпляра.

Статический метод не работает с определенным экземпляром, и это ошибка во время компиляции для ссылки this на статический метод.

Метод экземпляра работает с заданным экземпляром класса, и этот экземпляр можно получить как this (§12.8.14).

Различия между статическими и экземплярами рассматриваются далее в §15.3.8.

Виртуальные методы 15.6.4

Если объявление метода экземпляра включает в себя виртуальный модификатор, этот метод, как говорят, является виртуальным методом. Если виртуальный модификатор отсутствует, метод считается не виртуальным методом.

Реализация не-виртуального метода является инвариантной: реализация совпадает с тем, вызывается ли метод в экземпляре класса, в котором он объявлен или экземпляр производного класса. Напротив, реализация виртуального метода может быть заменена производными классами. Процесс замены реализации унаследованного виртуального метода называется переопределением этого метода (§15.6.5).

В вызове виртуального метода тип времени выполнения экземпляра, для которого выполняется вызов, определяет фактическую реализацию метода для вызова. В вызове, отличном от виртуального метода, тип времени компиляции экземпляра является определяющим фактором. В точных терминах, когда вызывается N метод с списком A аргументов в экземпляре с типом времени компиляции и типом CR времени выполнения (где R является либо C классом, производным от C), вызов обрабатывается следующим образом:

  • При привязке разрешение перегрузки применяется к , а для выбора определенного метода C из набора методов, объявленных и унаследованныхN.AMC Это описано в разделе "12.8.10.2".
  • Затем во время выполнения:
    • Если M это не виртуальный метод, M вызывается.
    • M В противном случае — это виртуальный метод, и вызывается наиболее производная реализация M в отношенииR.

Для каждого виртуального метода, объявленного в классе или унаследованного, существует наиболее производная реализация метода в отношении этого класса. Наиболее производная реализация виртуального метода M относительно класса R определяется следующим образом:

  • Если R содержит вводное виртуальное объявление M, то это наиболее производная реализация M в отношении R.
  • В противном случае, если R содержит переопределение M, то это наиболее производная реализация M в отношении R.
  • В противном случае наиболее производная реализация в отношении M является той же, что и наиболее производная реализация RM в отношении прямого базового классаR.

Пример. В следующем примере показаны различия между виртуальными и не виртуальными методами.

class A
{
    public void F() => Console.WriteLine("A.F");
    public virtual void G() => Console.WriteLine("A.G");
}

class B : A
{
    public new void F() => Console.WriteLine("B.F");
    public override void G() => Console.WriteLine("B.G");
}

class Test
{
    static void Main()
    {
        B b = new B();
        A a = b;
        a.F();
        b.F();
        a.G();
        b.G();
    }
}

В примере A представлен неиртуационный метод F и виртуальный метод G. B Класс вводит новый не-виртуальный методF, скрывая унаследованныйF, а также переопределяет унаследованный методG. Пример создает выходные данные:

A.F
B.F
B.G
B.G

Обратите внимание, что оператор a.G() вызывает B.G, а не A.G. Это связано с тем, что тип времени выполнения экземпляра (то Bесть), а не тип времени компиляции экземпляра (то есть A), определяет фактическую реализацию метода для вызова.

пример конца

Так как методы могут скрывать унаследованные методы, класс может содержать несколько виртуальных методов с одной и той же сигнатурой. Это не представляет проблему неоднозначности, так как все, кроме наиболее производного метода, скрыты.

Пример. В следующем коде

class A
{
    public virtual void F() => Console.WriteLine("A.F");
}

class B : A
{
    public override void F() => Console.WriteLine("B.F");
}

class C : B
{
    public new virtual void F() => Console.WriteLine("C.F");
}

class D : C
{
    public override void F() => Console.WriteLine("D.F");
}

class Test
{
    static void Main()
    {
        D d = new D();
        A a = d;
        B b = d;
        C c = d;
        a.F();
        b.F();
        c.F();
        d.F();
    }
}

классы C содержат два виртуальных метода с одной и той же сигнатурой: одна, представленная D и введеннаяA.C Метод, представленный путем C скрытия метода, унаследованного от A. Таким образом, объявление D переопределения переопределяет метод, представленный методом, и C невозможно переопределить метод, представленныйDA. Пример создает выходные данные:

B.F
B.F
D.F
D.F

Обратите внимание, что можно вызвать скрытый виртуальный метод путем доступа к экземпляру D через менее производный тип, в котором метод не скрыт.

пример конца

Методы переопределения 15.6.5

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

Переопределенный метод объявлением переопределения называется M для метода переопределения, объявленного в классеC, переопределенный базовый метод определяется путем изучения каждого базового классаC, начиная с прямого базового класса и продолжающегося с каждым последовательным прямым базовым классомC, пока в заданном типе базового класса по крайней мере один доступный метод находится с той же подписью, что M и после подстановки аргументов типа. В целях определения переопределенного базового метода метод считается доступным, если он имеет publicзначение , если он имеет значение , если он protectedесть, protected internalили internal если он либо или private protected объявлен в той же программе, что Cи .

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

  • Переопределенный базовый метод можно найти, как описано выше.
  • Существует именно один такой переопределенный базовый метод. Это ограничение действует, только если тип базового класса является созданным типом, где подстановка аргументов типа делает подпись двух методов одинаковой.
  • Переопределенный базовый метод — это виртуальный, абстрактный или переопределенный метод. Другими словами, переопределенный базовый метод не может быть статическим или не виртуальным.
  • Переопределенный базовый метод не является запечатанным методом.
  • Существует преобразование удостоверений между возвращаемым типом переопределенного базового метода и методом переопределения.
  • Объявление переопределения и переопределенный базовый метод имеют те же объявленные специальные возможности. Другими словами, объявление переопределения не может изменить доступность виртуального метода. Тем не менее, если переопределенный базовый метод защищен внутренним и он объявлен в другой сборке, отличной от сборки, содержащей объявление переопределения, то объявленное объявление переопределения должно быть защищено.
  • Объявление переопределения не указывает type_parameter_constraints_clause s. Вместо этого ограничения наследуются от переопределенного базового метода. Ограничения, которые являются параметрами типа в переопределенном методе, могут быть заменены аргументами типа в наследуемом ограничении. Это может привести к ограничениям, которые недопустимы при явном указании, таких как типы значений или запечатанные типы.

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

abstract class C<T>
{
    public virtual T F() {...}
    public virtual C<T> G() {...}
    public virtual void H(C<T> x) {...}
}

class D : C<string>
{
    public override string F() {...}            // Ok
    public override C<string> G() {...}         // Ok
    public override void H(C<T> x) {...}        // Error, should be C<string>
}

class E<T,U> : C<U>
{
    public override U F() {...}                 // Ok
    public override C<U> G() {...}              // Ok
    public override void H(C<T> x) {...}        // Error, should be C<U>
}

пример конца

Объявление переопределения может получить доступ к переопределенным базовым методу с помощью base_access (§12.8.15).

Пример. В следующем коде

class A
{
    int x;

    public virtual void PrintFields() => Console.WriteLine($"x = {x}");
}

class B : A
{
    int y;

    public override void PrintFields()
    {
        base.PrintFields();
        Console.WriteLine($"y = {y}");
    }
}

base.PrintFields() вызов вызывает B метод PrintFields, объявленный в A. Base_access отключает механизм виртуального вызова и просто обрабатывает базовый метод как метод, отличныйvirtual от метода. Если вызов был записанB, он будет рекурсивно вызывать ((A)this).PrintFields() метод, объявленный в , а не тот, который объявлен в PrintFieldsB, так как A является виртуальным и типом PrintFields времени выполнения является ((A)this).B

пример конца

Только путем включения override модификатора метод может переопределить другой метод. Во всех других случаях метод с той же сигнатурой, что и унаследованный метод, просто скрывает унаследованный метод.

Пример. В следующем коде

class A
{
    public virtual void F() {}
}

class B : A
{
    public virtual void F() {} // Warning, hiding inherited F()
}

F Метод в B не включает override модификатор и поэтому не переопределяет F метод в A. Скорее, метод в F скрытии метода Bи предупреждение сообщается, A так как объявление не включает новый модификатор.

пример конца

Пример. В следующем коде

class A
{
    public virtual void F() {}
}

class B : A
{
    private new void F() {} // Hides A.F within body of B
}

class C : B
{
    public override void F() {} // Ok, overrides A.F
}

F Метод в B скрытии виртуального F метода, унаследованного от A. Так как новый F в B закрытом доступе имеет частный доступ, его область включает только тело B класса и не распространяется на C. Поэтому объявление F в C разрешено переопределить F унаследованное от A.

пример конца

15.6.6 Запечатанные методы

Если объявление метода экземпляра включает sealed модификатор, этот метод считается запечатанным методом. Запечатанный метод переопределяет унаследованный виртуальный метод с той же сигнатурой. Запечатанный метод также должен быть помечен модификатором override . sealed Использование модификатора предотвращает дальнейшее переопределение метода производным классом.

Пример: пример

class A
{
    public virtual void F() => Console.WriteLine("A.F");
    public virtual void G() => Console.WriteLine("A.G");
}

class B : A
{
    public sealed override void F() => Console.WriteLine("B.F");
    public override void G()        => Console.WriteLine("B.G");
}

class C : B
{
    public override void G() => Console.WriteLine("C.G");
}

Класс B предоставляет два метода переопределения: F метод с модификатором sealed и методом G , который не имеет. BИспользование модификатора sealed предотвращает C дальнейшее переопределение F.

пример конца

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

Если объявление метода экземпляра включает abstract модификатор, этот метод считается абстрактным методом. Хотя абстрактный метод неявно также является виртуальным методом, он не может иметь модификатор virtual.

Объявление абстрактного метода вводит новый виртуальный метод, но не предоставляет реализацию этого метода. Вместо этого не абстрактные производные классы необходимы для предоставления собственной реализации путем переопределения этого метода. Поскольку абстрактный метод не предоставляет фактической реализации, текст метода абстрактного метода просто состоит из точки с запятой.

Объявления абстрактных методов разрешены только в абстрактных классах (§15.2.2.2).

Пример. В следующем коде

public abstract class Shape
{
    public abstract void Paint(Graphics g, Rectangle r);
}

public class Ellipse : Shape
{
    public override void Paint(Graphics g, Rectangle r) => g.DrawEllipse(r);
}

public class Box : Shape
{
    public override void Paint(Graphics g, Rectangle r) => g.DrawRect(r);
}

Shape Класс определяет абстрактное понятие объекта геометрической фигуры, который может закрасить себя. Метод Paint абстрактен, так как нет понятной реализации по умолчанию. Ellipse И Box классы являются конкретными Shape реализациями. Так как эти классы являются не абстрактными, они необходимы для переопределения Paint метода и предоставления фактической реализации.

пример конца

Это ошибка времени компиляции для base_access (§12.8.15) для ссылки на абстрактный метод.

Пример. В следующем коде

abstract class A
{
    public abstract void F();
}

class B : A
{
    // Error, base.F is abstract
    public override void F() => base.F();
}

Сообщается об ошибке во время компиляции для base.F() вызова, так как она ссылается на абстрактный метод.

пример конца

Объявление абстрактного метода разрешено переопределить виртуальный метод. Это позволяет абстрактным классам принудительно повторно реализовать метод в производных классах и делает исходную реализацию метода недоступной.

Пример. В следующем коде

class A
{
    public virtual void F() => Console.WriteLine("A.F");
}

abstract class B: A
{
    public abstract override void F();
}

class C : B
{
    public override void F() => Console.WriteLine("C.F");
}

класс A объявляет виртуальный метод, класс B переопределяет этот метод абстрактным методом, а класс C переопределяет абстрактный метод для предоставления собственной реализации.

пример конца

Внешние методы 15.6.8

Если объявление метода включает extern модификатор, метод считается внешним методом. Внешние методы реализуются внешне, как правило, с помощью языка, отличного от C#. Поскольку объявление внешнего метода не обеспечивает фактической реализации, текст метода внешнего метода просто состоит из запятой. Внешний метод не должен быть универсальным.

Механизм, с помощью которого достигается связывание с внешним методом, определяется реализацией.

Пример. В следующем примере показано использование extern модификатора и атрибута DllImport :

class Path
{
    [DllImport("kernel32", SetLastError=true)]
    static extern bool CreateDirectory(string name, SecurityAttribute sa);

    [DllImport("kernel32", SetLastError=true)]
    static extern bool RemoveDirectory(string name);

    [DllImport("kernel32", SetLastError=true)]
    static extern int GetCurrentDirectory(int bufSize, StringBuilder buf);

    [DllImport("kernel32", SetLastError=true)]
    static extern bool SetCurrentDirectory(string name);
}

пример конца

Частичные методы 15.6.9

Если объявление метода включает partial модификатор, этот метод, как говорят, является частичным методом. Частичные методы могут быть объявлены только как члены частичных типов (§15.2.7) и имеют ряд ограничений.

Частичные методы могут быть определены в одной части объявления типа и реализованы в другой. Реализация является необязательной; Если часть не реализует частичный метод, объявление частичного метода и все вызовы к нему удаляются из объявления типа, полученного из сочетания частей.

Частичные методы не определяют модификаторы доступа; они неявно закрыты. Их тип возвращаемого значения должен быть void, и их параметры не должны быть выходными параметрами. Частичный идентификатор распознается как контекстное ключевое слово (§6.4.4) в объявлении метода, только если оно отображается непосредственно перед ключевым словом void . Частичный метод не может явно реализовать методы интерфейса.

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

  • Объявления должны иметь одинаковые модификаторы (хотя и не обязательно в том же порядке), имя метода, количество параметров типа и количество параметров.
  • Соответствующие параметры в объявлениях должны иметь одинаковые модификаторы (хотя и не обязательно в том же порядке) и те же типы, или типы, преобразуемые удостоверения (различия модуля в именах параметров типа).
  • Соответствующие параметры типа в объявлениях должны иметь одинаковые ограничения (различия в модуле в именах параметров типа).

Реализующее объявление частичного метода может отображаться в той же части, что и соответствующее определение объявления частичного метода.

Только определяющий частичный метод участвует в разрешении перегрузки. Таким образом, задано ли объявление реализации, выражения вызова могут разрешаться для вызовов частичного метода. Так как частичный метод всегда возвращается void, такие выражения вызова всегда будут операторами выражений. Кроме того, поскольку частичный метод неявно private, такие инструкции всегда будут возникать в одной из частей объявления типа, в которых объявлен частичный метод.

Примечание. Определение сопоставления определений и реализации объявлений частичных методов не требует сопоставления имен параметров. Это может привести к удивительному, хотя и хорошо определенному, поведению при использовании именованных аргументов (§12.6.2.1). Например, при определении объявления частичного метода для M одного файла и реализации объявления частичного метода в другом файле:

// File P1.cs:
partial class P
{
    static partial void M(int x);
}

// File P2.cs:
partial class P
{
    static void Caller() => M(y: 0);
    static partial void M(int y) {}
}

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

конечная заметка

Если часть объявления частичного типа не содержит реализующего объявления для данного частичного метода, любая инструкция выражения, вызывающая ее, просто удаляется из объявления объединенного типа. Таким образом, выражение вызова, включая любые вложенные выражения, не влияет на время выполнения. Сам частичный метод также удаляется и не будет членом объявления объединенного типа.

Если для заданного частичного метода существует объявление реализации, вызовы частичных методов сохраняются. Частичный метод приводит к объявлению метода, аналогичному объявлению реализации частичного метода, за исключением следующего:

  • Модификатор partial не включен.

  • Атрибуты в итоговом объявлении метода — это объединенные атрибуты определения и реализации объявления частичного метода в неопределенном порядке. Дубликаты не удаляются.

  • Атрибуты параметров итогового объявления метода — это объединенные атрибуты соответствующих параметров определения и реализации объявления частичного метода в неопределенном порядке. Дубликаты не удаляются.

Если для частичного метода Mзадано определение объявления, но не реализующее объявление, применяются следующие ограничения:

  • Это ошибка времени компиляции для создания делегата из M (§12.8.17.6).

  • Это ошибка во время компиляции, которая ссылается M на анонимную функцию, преобразованную в тип дерева выражений (§8.6).

  • Выражения, происходящие в рамках вызова M , не влияют на определенное состояние назначения (§9.4), что может привести к ошибкам во время компиляции.

  • M не может быть точкой входа для приложения (§7.1).

Частичные методы полезны для разрешения одной части объявления типа для настройки поведения другой части, например той, которая создается средством. Рассмотрим следующее объявление частичного класса:

partial class Customer
{
    string name;

    public string Name
    {
        get => name;
        set
        {
            OnNameChanging(value);
            name = value;
            OnNameChanged();
        }
    }

    partial void OnNameChanging(string newName);
    partial void OnNameChanged();
}

Если этот класс компилируется без каких-либо других частей, определение объявлений частичных методов и их вызовов будет удалено, а итоговое объявление объединенного класса будет эквивалентно следующим:

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set => name = value;
    }
}

Предположим, что предоставляется другая часть, которая предоставляет объявления частичных методов:

partial class Customer
{
    partial void OnNameChanging(string newName) =>
        Console.WriteLine($"Changing {name} to {newName}");

    partial void OnNameChanged() =>
        Console.WriteLine($"Changed to {name}");
}

Затем итоговое объявление объединенного класса будет эквивалентно следующему:

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set
        {
            OnNameChanging(value);
            name = value;
            OnNameChanged();
        }
    }

    void OnNameChanging(string newName) =>
        Console.WriteLine($"Changing {name} to {newName}");

    void OnNameChanged() =>
        Console.WriteLine($"Changed to {name}");
}

Методы расширения 15.6.10

Если первый параметр метода включает this модификатор, этот метод считается методом расширения. Методы расширения объявляются только в не универсальных не вложенных статических классах. Первый параметр метода расширения ограничен следующим образом:

  • Он может быть только входным параметром, если он имеет тип значения
  • Это может быть только ссылочный параметр, если он имеет тип значения или имеет универсальный тип, ограниченный структурой
  • Он не должен быть типом указателя.

Пример. Ниже приведен пример статического класса, объявляющего два метода расширения:

public static class Extensions
{
    public static int ToInt32(this string s) => Int32.Parse(s);

    public static T[] Slice<T>(this T[] source, int index, int count)
    {
        if (index < 0 || count < 0 || source.Length - index < count)
        {
            throw new ArgumentException();
        }
        T[] result = new T[count];
        Array.Copy(source, index, result, 0, count);
        return result;
    }
}

пример конца

Метод расширения — это обычный статический метод. Кроме того, если его вложенный статический класс находится в области, метод расширения может вызываться с помощью синтаксиса вызова метода экземпляра (§12.8.10.3), используя выражение приемника в качестве первого аргумента.

Пример. В следующей программе используются методы расширения, объявленные выше:

static class Program
{
    static void Main()
    {
        string[] strings = { "1", "22", "333", "4444" };
        foreach (string s in strings.Slice(1, 2))
        {
            Console.WriteLine(s.ToInt32());
        }
    }
}

Метод Slice доступен для string[]метода , и ToInt32 метод доступен в string, так как они были объявлены как методы расширения. Значение программы совпадает со следующим значением, используя обычные статические вызовы метода:

static class Program
{
    static void Main()
    {
        string[] strings = { "1", "22", "333", "4444" };
        foreach (string s in Extensions.Slice(strings, 1, 2))
        {
            Console.WriteLine(Extensions.ToInt32(s));
        }
    }
}

пример конца

Текст метода 15.6.11

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

Абстрактные и внешние объявления методов не предоставляют реализацию метода, поэтому их тела методов просто состоят из точки с запятой. Для любого другого метода текст метода является блоком (§13.3), который содержит инструкции, выполняемые при вызове этого метода.

Действующий тип возвращаемого метода заключается void в том, имеет ли тип возвращаемого значения voidили если метод является асинхронным, а тип возвращаемого значения — «TaskType» (§15.15.1). В противном случае эффективный тип возвращаемого метода, не асинхронного, является его типом возвращаемого значения, а эффективный тип возвращаемого метода с типом возвращаемого значения «TaskType»<T>(§15.15.1).T

Если действующий тип возвращаемого метода void и метод имеет блоковое тело, return операторы (§13.10.5) в блоке не должны указывать выражение. Если выполнение блока метода void завершается нормально (т. е. потоки управления от конца текста метода), этот метод просто возвращается вызывающему объекту.

Если эффективный тип возвращаемого метода void и метод имеет тело выражения, выражение E должно быть statement_expression, а тело точно эквивалентно блоку формы { E; }.

Для метода return-by-value (§15.6.1) каждая инструкция return в тексте этого метода должна указывать выражение, которое неявно преобразуется в действующий тип возвращаемого значения.

Для метода return-by-ref (§15.6.1) каждая инструкция return в тексте этого метода должна указывать выражение, тип которого относится к эффективному типу возвращаемого типа, и имеет ссылочный безопасный контекст вызывающего контекста (§9.7.2).

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

Пример. В следующем коде

class A
{
    public int F() {} // Error, return value required

    public int G()
    {
        return 1;
    }

    public int H(bool b)
    {
        if (b)
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }

    public int I(bool b) => b ? 1 : 0;
}

Метод, возвращающий F значение, приводит к ошибке во время компиляции, так как элемент управления может вытекать из конца текста метода. G Методы H правильны, так как все возможные пути выполнения заканчиваются в операторе return, указывающего возвращаемое значение. Метод I правильный, так как его тело эквивалентно блоку с одной инструкцией возврата в нем.

пример конца

Свойства 15.7

15.7.1 Общие

Свойство — это элемент, предоставляющий доступ к характеристике объекта или класса. Примеры свойств включают длину строки, размер шрифта, подпись окна и имя клиента. Свойства — это естественное расширение полей, как именованные элементы с связанными типами, так и синтаксис для доступа к полям и свойствам одинаковы. Однако свойства, в отличие от полей, не указывают места хранения. Вместо этого свойства содержат методы доступа, в которых описаны инструкции для выполнения при чтении или записи значений. Таким образом, свойства предоставляют механизм связывания действий с чтением и записью характеристик объекта или класса; кроме того, они позволяют вычислить такие характеристики.

Свойства объявляются с помощью property_declaration:

property_declaration
    : attributes? property_modifier* type member_name property_body
    | attributes? property_modifier* ref_kind type member_name ref_property_body
    ;    

property_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;
    
property_body
    : '{' accessor_declarations '}' property_initializer?
    | '=>' expression ';'
    ;

property_initializer
    : '=' variable_initializer ';'
    ;

ref_property_body
    : '{' ref_get_accessor_declaration '}'
    | '=>' 'ref' variable_reference ';'
    ;

unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).

Существует два типа property_declaration:

  • Первый объявляет свойство, отличное от ссылок. Его значение имеет тип типа. Это свойство может быть доступно для чтения и /или записи.
  • Второй объявляет свойство ref-valued. Его значением является variable_reference (§9.5), которые могут быть readonlyпеременными типа. Это свойство доступно только для чтения.

Property_declaration может включать набор атрибутов (§22) и любой из разрешенных видов объявленной доступности (§15.3.6), (new), (§15.7.2static§15.6.4, virtual §15.7.6), (§15.6.5, override), (§15.6.6abstract, §15.7.6) и (extern) модификаторов.

Объявления свойств применяются к тем же правилам, что и объявления методов (§15.6) в отношении допустимых сочетаний модификаторов.

Member_name (§15.6.1) указывает имя свойства. Если свойство не является явной реализацией элемента интерфейса, member_name просто является идентификатором. Для явной реализации элемента интерфейса (§18.6.2) member_name состоит из interface_type , за которым следует "." и идентификатор.

Тип свойства должен быть по крайней мере доступным как сам свойство (§7.5.5).

Property_body может состоять из текста инструкции или текста выражения. В тексте инструкции accessor_declarations, которые должны быть заключены в маркеры "{" и "}", объявляют методы доступа (§15.7.3) свойства. Методы доступа указывают исполняемые инструкции, связанные с чтением и записью свойства.

В property_body тексте выражения, состоящем из => выражения, за которым следует выражениеE, и точка с запятой точно эквивалентна тексту { get { return E; } }инструкции, поэтому можно использовать только для указания свойств только для чтения, где результат метода доступа получается одним выражением.

Property_initializer может быть задано только для автоматически реализованного свойства (§15.7.4) и вызывает инициализацию базового поля таких свойств со значением, заданным выражением.

Ref_property_body может состоять из текста инструкции или текста выражения. В тексте инструкции get_accessor_declaration объявляет метод доступа (§15.7.3) свойства. Метод доступа задает исполняемые инструкции, связанные с чтением свойства.

В ref_property_body текст выражения, состоящий из => последующихref, V и точка с запятой точно эквивалентны тексту { get { return ref V; } }инструкции.

Примечание. Несмотря на то, что синтаксис для доступа к свойству аналогичен значению поля, свойство не классифицируется как переменная. Таким образом, невозможно передать свойство в качестве аргумента inили out аргумента, если свойство не имеет ссылочного значения и поэтому возвращает ссылку на переменную (ref). конечная заметка

Если объявление свойства включает extern модификатор, свойство, как считается, является внешним свойством. Поскольку объявление внешнего свойства не предоставляет фактической реализации, каждая из accessor_bodyв его accessor_declarations должна быть точкой с запятой.

Свойства статического и экземпляра 15.7.2

Если объявление свойства включает static модификатор, свойство, как утверждается, является статическим свойством. Если модификатор отсутствуетstatic, свойство считается свойством экземпляра.

Статичное свойство не связано с определенным экземпляром, и это ошибка во время компиляции, ссылающейся this на методы доступа статического свойства.

Свойство экземпляра связано с заданным экземпляром класса, и к данному экземпляру можно получить доступ как this (§12.8.14) в методах доступа этого свойства.

Различия между статическими и экземплярами рассматриваются далее в §15.3.8.

15.7.3 Методы доступа

Примечание. Это предложение относится к обоим свойствам (§15.7) и индексаторам (§15.9). Предложение записывается с точки зрения свойств, при чтении для индексаторов заменяющих индексатор/индексаторов свойств и свойств и ознакомьтесь со списком различий между свойствами и индексаторами, указанными в §15.9.2. конечная заметка

Accessor_declarations свойства указывают исполняемые инструкции, связанные с записью и/или чтением этого свойства.

accessor_declarations
    : get_accessor_declaration set_accessor_declaration?
    | set_accessor_declaration get_accessor_declaration?
    ;

get_accessor_declaration
    : attributes? accessor_modifier? 'get' accessor_body
    ;

set_accessor_declaration
    : attributes? accessor_modifier? 'set' accessor_body
    ;

accessor_modifier
    : 'protected'
    | 'internal'
    | 'private'
    | 'protected' 'internal'
    | 'internal' 'protected'
    | 'protected' 'private'
    | 'private' 'protected'
    ;

accessor_body
    : block
    | '=>' expression ';'
    | ';' 
    ;

ref_get_accessor_declaration
    : attributes? accessor_modifier? 'get' ref_accessor_body
    ;
    
ref_accessor_body
    : block
    | '=>' 'ref' variable_reference ';'
    | ';'
    ;

Accessor_declarations состоят из get_accessor_declaration, set_accessor_declaration или обоих. Каждое объявление доступа состоит из необязательных атрибутов, необязательных accessor_modifier, маркера get или setaccessor_body.

Для свойства ref-value, ref_get_accessor_declaration состоит из необязательных атрибутов, необязательных accessor_modifier маркера get, а затем ref_accessor_body.

Использование accessor_modifiers регулируется следующими ограничениями:

  • Accessor_modifier не следует использовать в интерфейсе или в явной реализации элемента интерфейса.
  • Для свойства или индексатора, не имеющего override модификатора, accessor_modifier разрешено только в том случае, если свойство или индексатор имеет метод доступа get и set, а затем разрешено только для одного из этих методов доступа.
  • Для свойства или индексатора, включающего override модификатор, метод доступа должен соответствовать accessor_modifier, если таковой имеется, переопределенного метода доступа.
  • Accessor_modifier должен объявить специальные возможности, строго ограничивающие, чем объявленные специальные возможности самого свойства или индексатора. Чтобы быть точным:
    • Если свойство или индексатор имеет объявленную доступность, то специальные возможностиpublic, объявленные accessor_modifier, могут иметь значение private protected, , protected internalinternalprotectedили .private
    • Если свойство или индексатор имеет объявленную доступность, то специальные возможностиprotected internal, объявленные accessor_modifier, могут иметь значение private protected, , protected privateinternalprotectedили .private
    • Если свойство или индексатор имеет объявленную доступность или , то специальные возможностиinternal, объявленные protected, должны быть либоprivate protected.private
    • Если свойство или индексатор имеет объявленную доступность, то специальные возможности private protected, объявленные accessor_modifier , должны быть private.
    • Если свойство или индексатор имеет объявленную доступностьprivate, accessor_modifier не может использоваться.

Для abstract свойств, extern отличных от ссылок, любые accessor_body для каждого указанного метода доступа — это просто точка с запятой. Не абстрактное, не экстерновое свойство, но не индексатор, может также иметь accessor_body для всех методов доступа, указанных как точка с запятой, в этом случае это автоматически реализованное свойство (§15.7.4). Автоматически реализованное свойство должно иметь по крайней мере метод доступа. Для методов доступа любого другого не абстрактного, неисключаемого свойства accessor_body:

  • блок, указывающий инструкции, выполняемые при вызове соответствующего метода доступа; или
  • текст выражения, состоящий из =>выражения и с запятой, и обозначает одно выражение, выполняемое при вызове соответствующего метода доступа.

Для abstract свойств, extern возвращаемых значением, ref_accessor_body — это просто точка с запятой. Для метода доступа любого другого не абстрактного, неисключаемого свойства ref_accessor_body:

  • блок, указывающий операторы, выполняемые при вызове метода доступа; или
  • текст выражения, состоящий из последующих =>ref, variable_reference и точки с запятой. Ссылка на переменную вычисляется при вызове метода доступа.

Метод доступа для свойства, отличного от ссылок, соответствует методу без параметров с возвращаемым значением типа свойства. За исключением целевого объекта назначения, если такое свойство ссылается в выражении, его метод доступа вызывается для вычисления значения свойства (§12.2.2).

Текст метода доступа, отличного от ссылок, должен соответствовать правилам методов возврата значений, описанным в разделе 15.6.11. В частности, все return операторы в тексте метода доступа get должны указывать выражение, которое неявно преобразуется в тип свойства. Кроме того, конечная точка метода доступа не может быть достигнута.

Метод доступа для свойства ref-value соответствует методу без параметров с возвращаемым значением variable_reference переменной типа свойства. Если такое свойство ссылается в выражении, его метод доступа вызывается для вычисления значения variable_reference свойства. Эта ссылка на переменную, как и любая другая, затем используется для чтения или для нечитаемых variable_references, записывайте указанную переменную в соответствии с контекстом.

Пример. В следующем примере показано свойство ref-valued в качестве целевого объекта назначения:

class Program
{
    static int field;
    static ref int Property => ref field;

    static void Main()
    {
        field = 10;
        Console.WriteLine(Property); // Prints 10
        Property = 20;               // This invokes the get accessor, then assigns
                                     // via the resulting variable reference
        Console.WriteLine(field);    // Prints 20
    }
}

пример конца

Текст метода доступа get для свойства ref-valued должен соответствовать правилам для методов, возвращаемых ref-valued, описанных в разделе 15.6.11.

Метод доступа набора соответствует методу с одним параметром значения типа свойства и возвращаемого void типа. Неявный параметр набора доступа всегда называется value. Если свойство ссылается в качестве целевого объекта назначения (§12.21), либо как операнды или ++ (–-§12.8.16, §12.9.6), метод доступа к набору вызывается с аргументом, предоставляющим новое значение (§12.21.21.2). Текст набора доступа должен соответствовать правилам void методов, описанным в разделе 15.6.11. В частности, операторы возврата в тексте набора доступа не допускаются для указания выражения. Так как метод доступа набора неявно имеет параметр с именем value, это ошибка во время компиляции для локальной переменной или объявления констант в наборе доступа, чтобы иметь это имя.

В зависимости от наличия или отсутствия методов доступа get и set свойство классифицируется следующим образом:

  • Свойство, которое включает как метод доступа, так и набор доступа, как говорят, является свойством чтения и записи.
  • Свойство, которое имеет только метод доступа для чтения, как говорят, является свойством только для чтения. Это ошибка во время компиляции для свойства только для чтения, которое должно быть целевым объектом назначения.
  • Свойство, которое имеет только набор доступа, как говорят, является свойством только для записи. За исключением целевого объекта назначения, это ошибка во время компиляции для ссылки на свойство только для записи в выражении.

Примечание. Пре- и постфикс ++-- и операторы и составные операторы присваивания не могут применяться к свойствам только для записи, так как эти операторы считывают старое значение операнда перед записью нового. конечная заметка

Пример. В следующем коде

public class Button : Control
{
    private string caption;

    public string Caption
    {
        get => caption;
        set
        {
            if (caption != value)
            {
                caption = value;
                Repaint();
            }
        }
    }

    public override void Paint(Graphics g, Rectangle r)
    {
        // Painting code goes here
    }
}

Элемент Button управления объявляет общедоступное Caption свойство. Метод доступа свойства Caption возвращает string хранящийся в частном caption поле. Метод доступа набора проверяет, отличается ли новое значение от текущего значения, а если да, он сохраняет новое значение и переопределяет элемент управления. Свойства часто следуют приведенному выше шаблону: метод доступа просто возвращает значение, хранящееся в private поле, и метод доступа задает это private поле, а затем выполняет дополнительные действия, необходимые для полного обновления состояния объекта. Учитывая приведенный Button выше класс, ниже приведен пример использования Caption свойства:

Button okButton = new Button();
okButton.Caption = "OK"; // Invokes set accessor
string s = okButton.Caption; // Invokes get accessor

Здесь метод доступа set вызывается путем назначения значения свойству, а метод доступа вызывается путем ссылки на свойство в выражении.

пример конца

Методы доступа к свойству get и set не являются отдельными элементами, и нельзя объявлять методы доступа свойства отдельно.

Пример: пример

class A
{
    private string name;

    // Error, duplicate member name
    public string Name
    { 
        get => name;
    }

    // Error, duplicate member name
    public string Name
    { 
        set => name = value;
    }
}

не объявляет одно свойство чтения и записи. Скорее, он объявляет два свойства с одинаковым именем, одним только для чтения и одним только записью. Так как два члена, объявленные в одном классе, не могут иметь одинаковое имя, в примере возникает ошибка во время компиляции.

пример конца

Когда производный класс объявляет свойство по тому же имени, что и унаследованное свойство, производное свойство скрывает унаследованное свойство в отношении чтения и записи.

Пример. В следующем коде

class A
{
    public int P
    {
        set {...}
    }
}

class B : A
{
    public new int P
    {
        get {...}
    }
}

P Свойство в BP скрытии A свойства в отношении чтения и записи. Таким образом, в инструкциях

B b = new B();
b.P = 1;       // Error, B.P is read-only
((A)b).P = 1;  // Ok, reference to A.P

Назначение, которое b.P вызывает сообщение об ошибке во время компиляции, так как свойство только P для чтения скрывает свойство B только P для записи в A. Обратите внимание, что приведение можно использовать для доступа к скрытому P свойству.

пример конца

В отличие от открытых полей, свойства обеспечивают разделение внутреннего состояния объекта и его общедоступного интерфейса.

Пример. Рассмотрим следующий код, который использует структуру Point для представления расположения:

class Label
{
    private int x, y;
    private string caption;

    public Label(int x, int y, string caption)
    {
        this.x = x;
        this.y = y;
        this.caption = caption;
    }

    public int X => x;
    public int Y => y;
    public Point Location => new Point(x, y);
    public string Caption => caption;
}

Label Здесь класс использует два int поля x и yсохраняет его расположение. Расположение предоставляется как в X виде, так и Y в свойстве, а также в качестве Location свойства типа Point. Если в будущей версии Labelстановится удобнее хранить расположение Point как внутренне, изменение может быть сделано без влияния на общедоступный интерфейс класса:

class Label
{
    private Point location;
    private string caption;

    public Label(int x, int y, string caption)
    {
        this.location = new Point(x, y);
        this.caption = caption;
    }

    public int X => location.X;
    public int Y => location.Y;
    public Point Location => location;
    public string Caption => caption;
}

Если x бы и y вместо этого были public readonly поля, это было бы невозможно сделать такое изменение класса Label .

пример конца

Примечание. Предоставление состояния через свойства не обязательно является менее эффективным, чем предоставление полей напрямую. В частности, если свойство не является виртуальным и содержит только небольшой объем кода, среда выполнения может заменить вызовы методов доступа фактическим кодом методов доступа. Этот процесс называется встраиванием, и он делает доступ к свойствам максимально эффективным, как доступ к полям, но сохраняет повышенную гибкость свойств. конечная заметка

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

class Counter
{
    private int next;

    public int Next => next++;
}

Значение Next свойства зависит от количества ранее доступ к свойству. Таким образом, доступ к свойству создает наблюдаемый побочный эффект, а свойство должно быть реализовано как метод.

Соглашение "без побочных эффектов" для методов доступа не означает, что методы доступа всегда должны быть записаны просто для возврата значений, хранящихся в полях. Действительно, методы доступа часто вычисляют значение свойства, получая доступ к нескольким полям или вызывая методы. Однако правильно разработанный метод доступа не выполняет никаких действий, которые вызывают наблюдаемые изменения в состоянии объекта.

пример конца

Свойства можно использовать для задержки инициализации ресурса до момента его первой ссылки.

Пример:

public class Console
{
    private static TextReader reader;
    private static TextWriter writer;
    private static TextWriter error;

    public static TextReader In
    {
        get
        {
            if (reader == null)
            {
                reader = new StreamReader(Console.OpenStandardInput());
            }
            return reader;
        }
    }

    public static TextWriter Out
    {
        get
        {
            if (writer == null)
            {
                writer = new StreamWriter(Console.OpenStandardOutput());
            }
            return writer;
        }
    }

    public static TextWriter Error
    {
        get
        {
            if (error == null)
            {
                error = new StreamWriter(Console.OpenStandardError());
            }
            return error;
        }
    }
...
}

Класс Console содержит три свойства, InOutи Error, которые представляют стандартные входные, выходные и ошибки устройства соответственно. Предоставляя эти элементы в качестве свойств, класс может отложить их инициализацию до тех пор, Console пока они не будут использованы. Например, при первом ссылке Out на свойство, как в

Console.Out.WriteLine("hello, world");

Создается базовый TextWriter элемент для выходного устройства. Однако если приложение не ссылается на In свойства и Error свойства, то для этих устройств не создаются объекты.

пример конца

15.7.4 Автоматически реализованные свойства

Автоматически реализованное свойство (или автоматическое свойство для короткого) — это не абстрактное, неисправное, неотражаемое свойство с точкой с запятой accessor_body. Автоматические свойства должны иметь метод доступа к методу доступа и при необходимости может иметь метод доступа к набору.

Если свойство указывается как автоматически реализованное свойство, скрытое поле резервного копирования автоматически доступно для свойства, а методы доступа реализуются для чтения и записи в это резервное поле. Скрытое резервное поле недоступно, оно может быть прочитано и записано только с помощью автоматически реализованных методов доступа к свойствам, даже в пределах содержащего типа. Если свойство auto-property не имеет метода доступа, то поле резервного копирования считается readonly (§15.5.3). readonly Как и поле, автоматическое свойство только для чтения также может быть назначено в тексте конструктора включающем классе. Такое назначение назначается непосредственно в поле резервной копии только для чтения свойства.

При необходимости для автоматического свойства может быть property_initializer, который применяется непосредственно к резервному полю в качестве variable_initializer (§17.7).

Пример:

public class Point
{
    public int X { get; set; } // Automatically implemented
    public int Y { get; set; } // Automatically implemented
}

эквивалентен следующему объявлению:

public class Point
{
    private int x;
    private int y;

    public int X { get { return x; } set { x = value; } }
    public int Y { get { return y; } set { y = value; } }
}

пример конца

Пример. В следующем примере

public class ReadOnlyPoint
{
    public int X { get; }
    public int Y { get; }

    public ReadOnlyPoint(int x, int y)
    {
        X = x;
        Y = y;
    }
}

эквивалентен следующему объявлению:

public class ReadOnlyPoint
{
    private readonly int __x;
    private readonly int __y;
    public int X { get { return __x; } }
    public int Y { get { return __y; } }

    public ReadOnlyPoint(int x, int y)
    {
        __x = x;
        __y = y;
    }
}

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

пример конца

Хотя резервное поле скрыто, это поле может иметь атрибуты, предназначенные для полей, применяются непосредственно к нему с помощью автоматически реализованных property_declaration свойства (§15.7.1).

Пример: следующий код

[Serializable]
public class Foo
{
    [field: NonSerialized]
    public string MySecret { get; set; }
}

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

[Serializable]
public class Foo
{
    [NonSerialized]
    private string _mySecretBackingField;
    public string MySecret
    {
        get { return _mySecretBackingField; }
        set { _mySecretBackingField = value; }
    }
}

пример конца

Специальные возможности 15.7.5

Если метод доступа имеет accessor_modifier, домен специальных возможностей (§7.5.3) метода доступа определяется с помощью объявленной доступности accessor_modifier. Если у метода доступа нет accessor_modifier, домен специальных возможностей метода доступа определяется из объявленной доступности свойства или индексатора.

Наличие accessor_modifier никогда не влияет на поиск элементов (§12.5) или разрешение перегрузки (§12.6.4). Модификаторы свойства или индексатора всегда определяют, к каким свойствам или индексатору привязаны независимо от контекста доступа.

После выбора определенного свойства, отличного от ref-value или неref-valued indexer, домены специальных возможностей определенных средств доступа используются для определения допустимости использования:

  • Если использование является значением (§12.2.2), метод доступа должен существовать и быть доступным.
  • Если использование является целевым объектом простого назначения (§12.21.2), метод доступа к набору должен существовать и быть доступным.
  • Если использование является целевым объектом составного назначения (§12.21.4--§12.8.16, §12.9.6), как методы доступа, так и набор доступа должны существовать и быть доступными.

Пример. В следующем примере свойство скрыто свойством A.TextB.Text, даже в контекстах, где вызывается только метод доступа набора. В отличие от этого, свойство B.Count недоступно для класса M, поэтому вместо этого используется доступное свойство A.Count .

class A
{
    public string Text
    {
        get => "hello";
        set { }
    }

    public int Count
    {
        get => 5;
        set { }
    }
}

class B : A
{
    private string text = "goodbye";
    private int count = 0;

    public new string Text
    {
        get => text;
        protected set => text = value;
    }

    protected new int Count
    {
        get => count;
        set => count = value;
    }
}

class M
{
    static void Main()
    {
        B b = new B();
        b.Count = 12;       // Calls A.Count set accessor
        int i = b.Count;    // Calls A.Count get accessor
        b.Text = "howdy";   // Error, B.Text set accessor not accessible
        string s = b.Text;  // Calls B.Text get accessor
    }
}

пример конца

После выбора определенного свойства ref-valued или ref-valued indexer; указывает, является ли использование значением, целевым объектом простого назначения или целевым объектом составного назначения; Домен специальных возможностей задействованного метода доступа используется для определения допустимости использования.

Метод доступа, используемый для реализации интерфейса, не должен иметь accessor_modifier. Если для реализации интерфейса используется только один метод доступа, другой метод доступа может быть объявлен с помощью accessor_modifier:

Пример:

public interface I
{
    string Prop { get; }
}

public class C : I
{
    public string Prop
    {
        get => "April";     // Must not have a modifier here
        internal set {...}  // Ok, because I.Prop has no set accessor
    }
}

пример конца

15.7.6 Виртуальные, запечатанные, переопределения и абстрактные методы доступа

Примечание. Это предложение относится к обоим свойствам (§15.7) и индексаторам (§15.9). Предложение записывается с точки зрения свойств, при чтении для индексаторов заменяющих индексатор/индексаторов свойств и свойств и ознакомьтесь со списком различий между свойствами и индексаторами, указанными в §15.9.2. конечная заметка

Объявление виртуального свойства указывает, что методы доступа свойства являются виртуальными. Модификатор virtual применяется ко всем не закрытым доступам свойства. Если метод доступа виртуального свойства имеет privateaccessor_modifier, частный метод доступа неявно не является виртуальным.

Объявление абстрактного свойства указывает, что методы доступа свойства являются виртуальными, но не предоставляют фактическую реализацию методов доступа. Вместо этого не абстрактные производные классы необходимы для предоставления собственной реализации для методов доступа путем переопределения свойства. Поскольку метод доступа для объявления абстрактного свойства не предоставляет фактической реализации, его accessor_body просто состоит из точки с запятой. Абстрактное свойство не должно иметь private метод доступа.

Объявление свойства, включающее abstractoverride и модификаторы, указывает, что свойство абстрактно и переопределяет базовое свойство. Методы доступа такого свойства также абстрактны.

Объявления абстрактных свойств разрешены только в абстрактных классах (§15.2.2.2). Методы доступа унаследованного виртуального свойства можно переопределить в производном классе, включив объявление свойства, указывающее директиву override . Это называется объявлением переопределения свойств. Объявление переопределяющего свойства не объявляет новое свойство. Вместо этого он просто специализируется на реализации методов доступа существующего виртуального свойства.

Объявление переопределения и переопределенное базовое свойство должны иметь те же объявленные специальные возможности. Другими словами, объявление переопределения не должно изменять доступность базового свойства. Тем не менее, если переопределенное базовое свойство защищено внутренним и оно объявляется в другой сборке, отличной от сборки, содержащей объявление переопределения, то объявленное объявление переопределения должно быть защищено. Если унаследованное свойство имеет только один метод доступа (т. е. если унаследованное свойство доступно только для чтения или только для записи), то переопределяющее свойство должно включать только этот метод доступа. Если унаследованное свойство включает оба метода доступа (т. е. если унаследованное свойство является чтением и записью), то переопределяющее свойство может включать один метод доступа или оба метода доступа. Должно быть преобразование удостоверений между типом переопределения и унаследованным свойством.

Объявление свойства переопределения может включать sealed модификатор. Использование этого модификатора предотвращает дальнейшее переопределение свойства производным классом. Также запечатаны методы доступа к запечатанном свойству.

За исключением различий в синтаксисе объявления и вызова, виртуальных, запечатанных, переопределениях и абстрактных методах доступа работают точно так же, как виртуальные, запечатанные, переопределения и абстрактные методы. В частности, правила, описанные в §15.6.4, §15.6.5, §15.6.6 и §15.6.7 применяются, как если бы методы доступа были методами соответствующей формы:

  • Метод доступа get соответствует методу без параметров с возвращаемым значением типа свойства и теми же модификаторами, что и содержащее свойство.
  • Метод доступа набора соответствует методу с одним параметром значения типа свойства, типом возвращаемого пустоты и теми же модификаторами, что и содержащее свойство.

Пример. В следующем коде

abstract class A
{
    int y;

    public virtual int X
    {
        get => 0;
    }

    public virtual int Y
    {
        get => y;
        set => y = value;
    }

    public abstract int Z { get; set; }
}

X является свойством только для чтения, Y является свойством виртуальной записи для чтения и записи и Z является абстрактным свойством чтения и записи. Поскольку Z это абстрактно, содержащий класс A также должен быть объявлен абстрактным.

Ниже показан класс, производный от A следующего:

class B : A
{
    int z;

    public override int X
    {
        get => base.X + 1;
    }

    public override int Y
    {
        set => base.Y = value < 0 ? 0: value;
    }

    public override int Z
    {
        get => z;
        set => z = value;
    }
}

Здесь объявления и XY переопределяют объявления Zсвойств. Каждое объявление свойства точно соответствует модификаторам специальных возможностей, типу и имени соответствующего унаследованного свойства. Метод доступа X и метод доступа к набору для доступа к унаследованным доступам с помощью базового ключевого Y слова. Объявление Z переопределяет как абстрактные методы доступа, таким образом, в нем отсутствуют выдающиеся abstract члены Bфункции и B разрешено быть не абстрактным классом.

пример конца

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

Пример:

public class B
{
    public virtual int P
    {
        get {...}
        protected set {...}
    }
}

public class D: B
{
    public override int P
    {
        get {...}            // Must not have a modifier here
        protected set {...}  // Must specify protected here
    }
}

пример конца

15.8 События

15.8.1 Общие

Событие — это член, который позволяет объекту или классу предоставлять уведомления. Клиенты могут присоединить исполняемый код для событий, предоставив обработчики событий.

События объявляются с помощью event_declarations:

event_declaration
    : attributes? event_modifier* 'event' type variable_declarators ';'
    | attributes? event_modifier* 'event' type member_name
        '{' event_accessor_declarations '}'
    ;

event_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

event_accessor_declarations
    : add_accessor_declaration remove_accessor_declaration
    | remove_accessor_declaration add_accessor_declaration
    ;

add_accessor_declaration
    : attributes? 'add' block
    ;

remove_accessor_declaration
    : attributes? 'remove' block
    ;

unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).

Event_declaration может включать набор атрибутов (§22) и любой из разрешенных видов объявленной доступности (§15.3.6), (new), (§15.6.3, static), (§15.6.4, virtual §15.8.5), (§15.6.5, override), (§15.6.6abstract, §15.8.5) и (extern) модификаторов.

Объявления событий применяются к тем же правилам, что и объявления методов (§15.6) в отношении допустимых сочетаний модификаторов.

Тип объявления события должен быть delegate_type (§8.2.8), и что delegate_type должно быть по крайней мере так же доступно, как само событие (§7.5.5).

Объявление события может включать event_accessor_declarations. Однако если это не так, для неисстрактных событий компилятор должен предоставлять их автоматически (§15.8.2); для событий extern методы доступа предоставляются внешним образом.

Объявление события, в котором event_accessor_declaration event_accessor_declaration определяется одно или несколько событий — по одному для каждого из variable_declarator. Атрибуты и модификаторы применяются ко всем элементам, объявленным таким event_declaration.

Это ошибка во время компиляции для event_declaration для включения модификатора и abstract.

Если объявление события включает extern модификатор, событие считается внешним событием. Так как объявление внешнего события не обеспечивает фактической реализации, это ошибка для включения extern модификатора и event_accessor_declarations.

Это ошибка во время компиляции для variable_declarator объявления события с abstract модификатором или external модификатором для включения variable_initializer.

Событие можно использовать в качестве левого += операнда операторов и -= операторов. Эти операторы используются соответственно для присоединения обработчиков событий к событию или удаления обработчиков событий из события, а также модификаторов доступа элемента управления событиями контекстов, в которых разрешены такие операции.

Единственные операции, разрешенные для события по коду, который находится за пределами типа, в котором объявлено это событие, и +=-=. Таким образом, хотя такой код может добавлять и удалять обработчики для события, он не может напрямую получить или изменить базовый список обработчиков событий.

В операции формы или , когда x += y является событием, результатом операции является тип x –= y (§12.21.5xв отличие от типа voidназначения, а также другие xx операторы, определенные в типах не-событий).+=-= Это предотвращает косвенное изучение базового делегата события внешним кодом.

Пример. В следующем примере показано, как обработчики событий присоединены к экземплярам Button класса:

public delegate void EventHandler(object sender, EventArgs e);

public class Button : Control
{
    public event EventHandler Click;
}

public class LoginDialog : Form
{
    Button okButton;
    Button cancelButton;

    public LoginDialog()
    {
        okButton = new Button(...);
        okButton.Click += new EventHandler(OkButtonClick);
        cancelButton = new Button(...);
        cancelButton.Click += new EventHandler(CancelButtonClick);
    }

    void OkButtonClick(object sender, EventArgs e)
    {
        // Handle okButton.Click event
    }

    void CancelButtonClick(object sender, EventArgs e)
    {
        // Handle cancelButton.Click event
    }
}

LoginDialog Здесь конструктор экземпляра создает два Button экземпляра и присоединяет обработчики событий к событиямClick.

пример конца

15.8.2 События, подобные полям

В тексте программы класса или структуры, содержащей объявление события, некоторые события можно использовать как поля. Для использования таким образом событие не должно быть абстрактным или экстернным, и не должно явно включать event_accessor_declarations. Такое событие можно использовать в любом контексте, который разрешает поле. Поле содержит делегат (§20), который ссылается на список обработчиков событий, добавленных в событие. Если обработчики событий не были добавлены, поле содержит null.

Пример. В следующем коде

public delegate void EventHandler(object sender, EventArgs e);

public class Button : Control
{
    public event EventHandler Click;

    protected void OnClick(EventArgs e)
    {
        EventHandler handler = Click;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    public void Reset() => Click = null;
}

Click используется в качестве поля в Button классе. Как показано в примере, поле можно просматривать, изменять и использовать в выражениях вызова делегата. Метод OnClick в Button классе "вызывает" Click событие. Концепция создания события в точности соответствует вызову делегата, представленного этим событием. Это позволяет обойтись без особой языковой конструкции для создания событий. Обратите внимание, что перед вызовом делегата предшествует проверка, которая гарантирует, что делегат не имеет значения NULL и что проверка выполняется в локальной копии, чтобы обеспечить безопасность потока.

Вне объявления Button класса Click член может использоваться только в левой части += и –= операторов, как и в

b.Click += new EventHandler(...);

который добавляет делегат в список вызовов Click события и

Click –= new EventHandler(...);

, который удаляет делегат из списка вызовов Click события.

пример конца

При компиляции события, подобного полю, компилятор автоматически создает хранилище для удержания делегата и создает методы доступа для события, добавляющего или удаляющего обработчики событий в поле делегата. Операции добавления и удаления являются потокобезопасным и могут (но не требуются) выполняться при удержании блокировки (§13.13) на содержащего объекта для события экземпляра или System.Type объекта (§12.8.18) для статического события.

Примечание. Таким образом, объявление события экземпляра формы:

class X
{
    public event D Ev;
}

должен быть скомпилирован в что-то эквивалентное:

class X
{
    private D __Ev; // field to hold the delegate

    public event D Ev
    {
        add
        {
            /* Add the delegate in a thread safe way */
        }
        remove
        {
            /* Remove the delegate in a thread safe way */
        }
    }
}

Ссылки на класс XEv в левой части += и –= операторы вызывают вызовы к добавлению и удалению методов доступа. Все остальные ссылки Ev компилируются для ссылки на скрытое поле __Ev (§12.8.7). Имя "__Ev" является произвольным; скрытое поле может иметь любое имя или ни одно имя вообще.

конечная заметка

15.8.3 Методы доступа к событиям

Примечание. Объявления событий обычно не event_accessor_declaration s, как показано в приведенном Button выше примере. Например, они могут быть включены, если стоимость хранения одного поля на событие неприемлема. В таких случаях класс может включать event_accessor_declarationи использовать частный механизм для хранения списка обработчиков событий. конечная заметка

Event_accessor_declarations события указывают исполняемые инструкции, связанные с добавлением и удалением обработчиков событий.

Объявления доступа состоят из add_accessor_declaration и remove_accessor_declaration. Каждое объявление метода доступа состоит из добавления или удаления маркера, за которым следует блок. Блок, связанный с add_accessor_declaration, указывает инструкции, выполняемые при добавлении обработчика событий, а блок, связанный с remove_accessor_declaration, указывает инструкции для выполнения при удалении обработчика событий.

Каждый add_accessor_declaration и remove_accessor_declaration соответствует методу с одним параметром значения типа события и возвращаемым типомvoid. Неявный параметр метода доступа к событиям называется value. При использовании события в назначении событий используется соответствующий метод доступа к событиям. В частности, если оператор назначения используется += , то используется метод доступа к добавлению, а если оператор назначения является –= методом удаления. В любом случае правый операнды оператора присваивания используется в качестве аргумента для метода доступа к событиям. Блок add_accessor_declaration или remove_accessor_declaration должен соответствовать правилам методовvoid, описанным в разделе 15.6.9. В частности, return операторы в таком блоке не допускаются для указания выражения.

Так как метод доступа к событиям неявно имеет параметр с именем value, это ошибка во время компиляции для локальной переменной или константы, объявленной в методе доступа к событиям, чтобы иметь это имя.

Пример. В следующем коде


class Control : Component
{
    // Unique keys for events
    static readonly object mouseDownEventKey = new object();
    static readonly object mouseUpEventKey = new object();

    // Return event handler associated with key
    protected Delegate GetEventHandler(object key) {...}

    // Add event handler associated with key
    protected void AddEventHandler(object key, Delegate handler) {...}

    // Remove event handler associated with key
    protected void RemoveEventHandler(object key, Delegate handler) {...}

    // MouseDown event
    public event MouseEventHandler MouseDown
    {
        add { AddEventHandler(mouseDownEventKey, value); }
        remove { RemoveEventHandler(mouseDownEventKey, value); }
    }

    // MouseUp event
    public event MouseEventHandler MouseUp
    {
        add { AddEventHandler(mouseUpEventKey, value); }
        remove { RemoveEventHandler(mouseUpEventKey, value); }
    }

    // Invoke the MouseUp event
    protected void OnMouseUp(MouseEventArgs args)
    {
        MouseEventHandler handler;
        handler = (MouseEventHandler)GetEventHandler(mouseUpEventKey);
        if (handler != null)
        {
            handler(this, args);
        }
    }
}

Control Класс реализует внутренний механизм хранения событий. Метод AddEventHandler связывает значение делегата с ключом, GetEventHandler метод возвращает делегат, связанный с ключом, и RemoveEventHandler метод удаляет делегат в качестве обработчика событий для указанного события. Предположительно, базовый механизм хранения разработан таким образом, что нет затрат на связывание значения делегата NULL с ключом, и поэтому необработанные события не используют хранилища.

пример конца

События статического и экземпляра 15.8.4

Если объявление события включает static модификатор, событие считается статическим событием. Если модификатор отсутствуетstatic, событие считается событием экземпляра.

Статическое событие не связано с определенным экземпляром, и это ошибка во время компиляции для ссылки this на методы доступа статического события.

Событие экземпляра связано с заданным экземпляром класса, и доступ к этому экземпляру можно получить как this (§12.8.14) в методах доступа этого события.

Различия между статическими и экземплярами рассматриваются далее в §15.3.8.

15.8.5 Виртуальные, запечатанные, переопределения и абстрактные методы доступа

Объявление виртуального события указывает, что методы доступа этого события являются виртуальными. Модификатор virtual применяется к обоим методом доступа события.

Объявление абстрактного события указывает, что методы доступа события являются виртуальными, но не предоставляют фактическую реализацию методов доступа. Вместо этого не абстрактные производные классы необходимы для предоставления собственной реализации для методов доступа путем переопределения события. Поскольку метод доступа для объявления абстрактного события не предоставляет фактической реализации, он не должен предоставлять event_accessor_declarations.

Объявление события, включающее abstractoverride и модификаторы, указывает, что событие абстрактно и переопределяет базовое событие. Методы доступа такого события также абстрактны.

Объявления абстрактных событий разрешены только в абстрактных классах (§15.2.2.2).

Методы доступа унаследованного виртуального события можно переопределить в производном классе, включив объявление события, указывающее override модификатор. Это называется переопределяющим объявлением события. Объявление события переопределения не объявляет новое событие. Вместо этого он просто специализируется на реализации методов доступа существующего виртуального события.

Объявление события переопределения должно указывать точно те же модификаторы специальных возможностей и имя, что и переопределенное событие, в объявлении должно быть преобразование удостоверений между типом переопределения и переопределенным событием, а также средства добавления и удаления доступа должны быть указаны в объявлении.

Объявление события переопределения может включать sealed модификатор. Использование модификатора this предотвращает дальнейшее переопределение события производным классом. Также запечатаны методы доступа к запечатанном событию.

Это ошибка во время компиляции для переопределения объявления события, включающего new модификатор.

За исключением различий в синтаксисе объявления и вызова, виртуальных, запечатанных, переопределениях и абстрактных методах доступа работают точно так же, как виртуальные, запечатанные, переопределения и абстрактные методы. В частности, правила, описанные в §15.6.4, §15.6.5, §15.6.6 и §15.6.7 применяются, как если бы методы доступа были методами соответствующей формы. Каждый метод доступа соответствует методу с одним параметром значения типа события, void возвращаемого типа и теми же модификаторами, что и содержащее событие.

Индексаторы 15.9

15.9.1 Общие

Индексатор — это элемент, который позволяет индексировать объект таким же образом, как массив. Индексаторы объявляются с помощью indexer_declarations:

indexer_declaration
    : attributes? indexer_modifier* indexer_declarator indexer_body
    | attributes? indexer_modifier* ref_kind indexer_declarator ref_indexer_body
    ;

indexer_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

indexer_declarator
    : type 'this' '[' parameter_list ']'
    | type interface_type '.' 'this' '[' parameter_list ']'
    ;

indexer_body
    : '{' accessor_declarations '}' 
    | '=>' expression ';'
    ;  

ref_indexer_body
    : '{' ref_get_accessor_declaration '}'
    | '=>' 'ref' variable_reference ';'
    ;

unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).

Существует два типа indexer_declaration:

  • Первый объявляет индексатор без ссылок. Его значение имеет тип типа. Этот тип индексатора может быть доступен для чтения и /или записи.
  • Второй объявляет индексатор с ref-значением. Его значением является variable_reference (§9.5), которые могут быть readonlyпеременными типа. Этот тип индексатора доступен только для чтения.

Indexer_declaration может включать набор атрибутов (§22) и любой из разрешенных видов объявленной специальных возможностей (§15.3.6), new (§15.3.5), (§15.5). virtual 6.4), override (§15.6.5), sealed (§15.6.6abstract§15.6.7extern§15.6.8) модификаторов.

Объявления индексатора применяются к тем же правилам, что и объявления методов (§15.6) в отношении допустимых сочетаний модификаторов, при этом одно исключение заключается в том, что static модификатор не допускается в объявлении индексатора.

Тип объявления индексатора указывает тип элемента индексатора, представленного объявлением.

Примечание. Так как индексаторы предназначены для использования в контекстах элементов массива, тип элемента терминов, определенный для массива, также используется с индексатором. конечная заметка

Если индексатор не является явной реализацией элемента интерфейса, за типом следует ключевое слово this. Для явной реализации члена интерфейса за типомследует interface_type, "." и ключевое слово this. В отличие от других элементов индексаторы не имеют определяемых пользователем имен.

Parameter_list задает параметры индексатора. Список параметров индексатора соответствует методу (§15.6.2), за исключением того, что должен быть указан хотя бы один параметр, и что thisмодификаторы параметров , refа out также модификаторы параметров запрещены.

Тип индексатора и каждого из типов, на которые ссылается parameter_list, должен быть по крайней мере так же доступен, как сам индексатор (§7.5.5).

Indexer_body может состоять из текста оператора (§15.7.1) или тела выражения (§15.6.1). В тексте инструкции accessor_declarations, которая должна быть заключена в маркеры "{" и "}", объявите методы доступа (§15.7.3) индексатора. Методы доступа указывают исполняемые инструкции, связанные с чтением и записью элементов индексатора.

В indexer_body текст выражения, состоящий из "=>", за которым следует выражение E и точка с запятой, точно эквивалентен тексту { get { return E; } }инструкции, и поэтому можно использовать только для указания индексаторов только для чтения, где результат метода доступа получается одним выражением.

Ref_indexer_body может состоять из текста инструкции или текста выражения. В тексте инструкции get_accessor_declaration объявляет метод доступа (§15.7.3) индексатора. Метод доступа задает исполняемые инструкции, связанные с чтением индексатора.

В ref_indexer_body текст выражения, состоящий из => последующихref, V и точка с запятой точно эквивалентны тексту { get { return ref V; } }инструкции.

Примечание. Несмотря на то, что синтаксис для доступа к элементу индексатора совпадает с тем, что для элемента массива, элемент индексатора не классифицируется как переменная. Таким образом, невозможно передать элемент индексатора в качестве inoutаргумента, ref если индексатор не имеет ссылочного значения и поэтому возвращает ссылку (§9.7). конечная заметка

Parameter_list индексатора определяет подпись (§7.6) индексатора. В частности, сигнатура индексатора состоит из числа и типов его параметров. Тип элемента и имена параметров не являются частью подписи индексатора.

Подпись индексатора должна отличаться от подписей всех остальных индексаторов, объявленных в одном классе.

Когда объявление индексатора включает extern модификатор, индексатор, как говорят, является внешним индексатором. Поскольку объявление внешнего индексатора не обеспечивает фактической реализации, каждая из accessor_bodyв его accessor_declarations должна быть точкой с запятой.

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

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 => 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[] (так как каждое значение бывшего занимает только один бит, а не один byte), но разрешает те же операции, что и одно bool[].

CountPrimes Следующий класс использует и классический BitArray алгоритм "сить" для вычисления числа праймеров от 2 до заданного максимума:

class CountPrimes
{
    static int Count(int max)
    {
        BitArray flags = new BitArray(max + 1);
        int count = 0;
        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 {count} primes between 2 and {max}");
    }
}

Обратите внимание, что синтаксис для доступа к элементам объекта BitArray точно совпадает с синтаксисом bool[].

В следующем примере показан класс сетки 26×10 с индексатором с двумя параметрами. Первый параметр должен быть буквой верхнего или нижнего регистра в диапазоне A–Z, а второй — целым числом в диапазоне 0–9.

class Grid
{
    const int NumRows = 26;
    const int NumCols = 10;
    int[,] cells = new int[NumRows, NumCols];

    public int this[char row, int col]
    {
        get
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            return cells[row - 'A', col];
        }
        set
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException ("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            cells[row - 'A', col] = value;
        }
    }
}

пример конца

15.9.2 Индексатор и различия свойств

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

  • Свойство определяется его именем, а индексатор определяется сигнатурой.
  • Доступ к свойству осуществляется через simple_name (§12.8.4) или member_access (§12.8.7), а элемент индексатора осуществляется через element_access (§12.8.12.3).
  • Свойство может быть статическим элементом, в то время как индексатор всегда является членом экземпляра.
  • Метод доступа к свойству соответствует методу без параметров, в то время как метод доступа индексатора соответствует методу с тем же списком параметров, что и индексатор.
  • Метод доступа набора свойства соответствует методу с одним параметром с именем value, а метод доступа к набору индексатора соответствует методу с тем же списком параметров, что и индексатор, а также дополнительный параметр с именем value.
  • Это ошибка во время компиляции для метода доступа индексатора для объявления локальной переменной или локальной константы с тем же именем, что и параметр индексатора.
  • В объявлении переопределения свойств наследуемое свойство обращается с помощью синтаксиса base.P, где P находится имя свойства. В объявлении индексатора переопределения наследуемый индексатор обращается с помощью синтаксиса base[E], где E находится разделенный запятыми список выражений.
  • Отсутствует концепция "автоматически реализованного индексатора". Это ошибка иметь не абстрактный, не внешний индексатор с точкой с запятой accessor_body.

Помимо этих различий, все правила, определенные в §15.7.3, §15.7.5 и §15.7.6 , применяются к средствам доступа индексатора, а также к средствам доступа к свойствам.

При чтении §15.7.3, §15.7.3, §15.7.5 и §15.7.6 также применяется к определенным терминам. В частности, свойство чтения и записи становится индексатором только для чтения, свойство только для чтения становится индексатором только для чтения, а свойство только для записи становится индексатором только для записи.

Операторы 15.10

15.10.1 Общие

Оператор — это член, определяющий смысл оператора выражения, который может применяться к экземплярам класса. Операторы объявляются с помощью operator_declarations:

operator_declaration
    : attributes? operator_modifier+ operator_declarator operator_body
    ;

operator_modifier
    : 'public'
    | 'static'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

operator_declarator
    : unary_operator_declarator
    | binary_operator_declarator
    | conversion_operator_declarator
    ;

unary_operator_declarator
    : type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
    ;

logical_negation_operator
    : '!'
    ;

overloadable_unary_operator
    : '+' | '-' | logical_negation_operator | '~' | '++' | '--' | 'true' | 'false'
    ;

binary_operator_declarator
    : type 'operator' overloadable_binary_operator
        '(' fixed_parameter ',' fixed_parameter ')'
    ;

overloadable_binary_operator
    : '+'  | '-'  | '*'  | '/'  | '%'  | '&' | '|' | '^'  | '<<' 
    | right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
    ;

conversion_operator_declarator
    : 'implicit' 'operator' type '(' fixed_parameter ')'
    | 'explicit' 'operator' type '(' fixed_parameter ')'
    ;

operator_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).

Примечание. Операторы префикса логического отрицания (§12.9.4) и постфиксов NULL-прощения операторов (§12.8.9), представленные тем же лексическим токеном (!), отличаются. Последний не является перегруженным оператором. конечная заметка

Существует три категории перегруженных операторов: унарные операторы (§15.10.2), двоичные операторы (§15.10.3) и операторы преобразования (§15.10.4).

Operator_body — это точка с запятой, тело блока (§15.6.1) или тело выражения (§15.6.1). Текст блока состоит из блока, который задает инструкции, выполняемые при вызове оператора. Блок должен соответствовать правилам для методов возврата значений, описанных в разделе 15.6.11. Текст выражения состоит из => выражения и точки с запятой и обозначает одно выражение для выполнения при вызове оператора.

Для extern операторов operator_body состоит просто из точки с запятой. Для всех остальных операторов operator_body — это блоковый текст или тело выражения.

Следующие правила применяются ко всем объявлениям операторов:

  • Объявление оператора должно включать как модификатор, public так и static модификатор.
  • Параметры оператора не должны иметь модификаторов, отличных от in.
  • Подпись оператора (§15.10.2, §15.10.3, §15.10.4) должна отличаться от подписей всех остальных операторов, объявленных в одном классе.
  • Все типы, на которые ссылаются в объявлении оператора, должны быть по крайней мере так же доступны, как сам оператор (§7.5.5).
  • Это ошибка для одного модификатора, который будет отображаться несколько раз в объявлении оператора.

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

Как и другие члены, операторы, объявленные в базовом классе, наследуются производными классами. Так как объявления операторов всегда требуют класса или структуры, в которой оператор объявляется для участия в подписи оператора, невозможно скрыть оператор, объявленный в производном классе, чтобы скрыть оператор, объявленный в базовом классе. Таким образом, new модификатор никогда не требуется и поэтому никогда не допускается в объявлении оператора.

Дополнительные сведения об унарных и двоичных операторах см. в статье 12.4.

Дополнительные сведения о операторах преобразования см. в разделе §10.5.

Унарные операторы 15.10.2

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

  • Унарный +, -! (только логический отрицание) или ~ оператор должен принимать один параметр типа T или T? может возвращать любой тип.
  • Унарный ++ или -- оператор должен принимать один параметр типа T или T? возвращать тот же тип или тип, производный от него.
  • Унарный true или false оператор должен принимать один параметр типа T или T? возвращать тип bool.

Сигнатура унарного оператора состоит из маркера оператора (+, , -!, ~++, --trueилиfalse) и типа одного параметра. Возвращаемый тип не является частью подписи унарного оператора и не является именем параметра.

Для true унарных false операторов требуется парное объявление. Ошибка во время компиляции возникает, если класс объявляет один из этих операторов без объявления другого. Операторы true описаны далее в false.

Пример. В следующем примере показана реализация и последующее использование оператора++ для целочисленного векторного класса:

public class IntVector
{
    public IntVector(int length) {...}
    public int Length { get { ... } }                      // Read-only property
    public int this[int index] { get { ... } set { ... } } // 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 в операнду, так же, как операторы добавочного и декремента постфикса (§12.8.16), а также операторы добавочного и декремента префикса (§12.9.6). В отличие от C++, этот метод не должен изменять значение операнда непосредственно, так как это нарушает стандартную семантику оператора добавочного префикса (§12.8.16).

пример конца

Двоичные операторы 15.10.3

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

  • Двоичный оператор, отличный от смены, должен принимать два параметра, по крайней мере один из которых должен иметь тип T или T?может возвращать любой тип.
  • Двоичный << или >> оператор (§12.11) должен принимать два параметра, первый из которых должен иметь тип или T? и второй из которых должен иметь тип Tint или int?, и может возвращать любой тип.

Подпись двоичного оператора состоит из маркера оператора (+, -*/%&|^<<>>==!=><>=или <=) и типов двух параметров. Возвращаемый тип и имена параметров не являются частью подписи двоичного оператора.

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

  • оператор и оператор ==!=
  • оператор и оператор ><
  • оператор и оператор >=<=

Операторы преобразования 15.10.4

Объявление оператора преобразования представляет определяемое пользователем преобразование (§10.5), которое расширяет предварительно определенные неявные и явные преобразования.

Объявление оператора преобразования, включающее implicit ключевое слово, представляет неявное преобразование, определяемое пользователем. Неявные преобразования могут выполняться в различных ситуациях, включая вызовы членов функции, выражения приведения и назначения. Это описано далее в разделе "10.2".

Объявление оператора преобразования, включающее explicit ключевое слово, представляет явное преобразование, определяемое пользователем. Явные преобразования могут возникать в выражениях приведения и описаны далее в разделе 10.3.

Оператор преобразования преобразуется из исходного типа, указанного типом параметра оператора преобразования, в целевой тип, указанный возвращаемым типом оператора преобразования.

Для заданного исходного типа и целевого типа ST, если S или T являются типами значений, допускающих значение NULL, давайте S₀ и T₀ ссылаемся на их базовые типы; в противном случае S₀ они равны и T₀S соответственноT. Класс или структуру разрешено объявлять преобразование из исходного типа S в целевой тип T , только если все из следующих значений имеют значение true:

  • S₀ и T₀ являются разными типами.

  • S₀ Либо T₀ или является типом экземпляра класса или структуры, содержащей объявление оператора.

  • Ни S₀T₀interface_type.

  • За исключением определяемых пользователем преобразований преобразование не существует из или из STTS.

В целях этих правил любые параметры типа, связанные с или 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
}

Первые два объявления операторов разрешены, так как T и intstringсоответственно считаются уникальными типами без связи. Однако третий оператор является ошибкой, так как C<T> является базовым классом D<T>.

пример конца

Из второго правила следует, что оператор преобразования должен преобразоваться в класс или из типа структуры, в который объявлен оператор.

Пример. Для класса или типа C структуры можно определить преобразование из Cint и из intCнего, но не из intboolнего. пример конца

Невозможно напрямую переопределить предварительно определенное преобразование. Таким образом, операторы преобразования не могут преобразовывать из object неявного и явного преобразования между object всеми другими типами. Аналогичным образом, ни исходные, ни целевые типы преобразования не могут быть базовым типом другого, так как преобразование уже существует. Однако можно объявить операторы универсальных типов, которые для определенных аргументов типов указывают преобразования, которые уже существуют в качестве предварительно определенных преобразований.

Пример:

struct Convertible<T>
{
    public static implicit operator Convertible<T>(T value) {...}
    public static explicit operator T(Convertible<T> value) {...}
}

если тип указан в качестве аргумента типа object для T, второй оператор объявляет преобразование, которое уже существует (неявное, а следовательно, явное преобразование существует из любого типа в объект типа).

пример конца

В случаях, когда предварительно определенное преобразование существует между двумя типами, любые пользовательские преобразования между этими типами игнорируются. В частности:

  • Если предварительно определенное неявное преобразование (§10.2) существует из типа в типST, все пользовательские преобразования (неявные или явные) S из них T игнорируются.
  • Если предварительно определенное явное преобразование (§10.3) существует от типа к типуST, все определяемые пользователем явные преобразования из S них T игнорируются. Кроме того:
    • S Если тип интерфейса или T является типом интерфейса, определяемые пользователем неявные преобразования ST игнорируются.
    • В противном случае определяемые пользователем неявные преобразования ST по-прежнему считаются.

Для всех типов, но 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
}

пример конца

Определяемые пользователем преобразования не допускаются для преобразования из interface_type или в interface_type. В частности, это ограничение гарантирует отсутствие определяемых пользователем преобразований при преобразовании в interface_type, а преобразование в interface_type выполняется только в том случае, если преобразованный object фактически реализует указанные interface_type.

Подпись оператора преобразования состоит из исходного типа и целевого типа. (Это единственная форма элемента, для которого возвращаемый тип участвует в сигнатуре.) Неявная или явная классификация оператора преобразования не является частью подписи оператора. Таким образом, класс или структура не может объявлять как неявный, так и явный оператор преобразования с одинаковыми исходными и целевыми типами.

Примечание. Как правило, определяемые пользователем неявные преобразования должны быть разработаны для того, чтобы никогда не создавать исключения и никогда не терять информацию. Если определяемое пользователем преобразование может привести к возникновению исключений (например, из-за отсутствия диапазона исходного аргумента) или потери информации (например, отмены битов высокого порядка), то это преобразование должно быть определено как явное преобразование. конечная заметка

Пример. В следующем коде

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) => d.value;
    public static explicit operator Digit(byte b) => new Digit(b);
}

Преобразование из Digitbyte в неявное, так как оно никогда не создает исключения или теряет информацию, но преобразование из byteDigit него является явным, так как Digit может представлять только подмножество возможных значенийbyte.

пример конца

Конструкторы экземпляров 15.11

15.11.1 Общие

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

constructor_declaration
    : attributes? constructor_modifier* constructor_declarator constructor_body
    ;

constructor_modifier
    : 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

constructor_declarator
    : identifier '(' parameter_list? ')' constructor_initializer?
    ;

constructor_initializer
    : ':' 'base' '(' argument_list? ')'
    | ':' 'this' '(' argument_list? ')'
    ;

constructor_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).

Constructor_declaration может включать набор атрибутов (§22), любой из разрешенных видов объявленной специальных возможностей (§15.3.6extern§15.6.8). Объявление конструктора не допускается включать один и тот же модификатор несколько раз.

Идентификатор constructor_declarator должен называть класс, в котором объявлен конструктор экземпляра. Если указано другое имя, возникает ошибка во время компиляции.

Необязательный parameter_list конструктора экземпляра применяется к тем же правилам, что и parameter_list метода (§15.6). this Поскольку модификатор для параметров применяется только к методам расширения (§15.6.10), ни в parameter_list конструктора не должен содержать this модификатор. Список параметров определяет сигнатуру (§7.6) конструктора экземпляра и управляет процессом разрешения перегрузки (§12.6.4) выбирает конкретный конструктор экземпляра в вызове.

Каждый из типов, на которые ссылается в parameter_list конструктора экземпляра, должен быть по крайней мере так же доступен, как сам конструктор (§7.5.5).

Необязательный constructor_initializer указывает другой конструктор экземпляра для вызова перед выполнением инструкций, заданных в constructor_body этого конструктора экземпляра. Это описано далее в §15.11.2.

Когда объявление конструктора включает extern модификатор, конструктор считается внешним конструктором. Поскольку объявление внешнего конструктора не предоставляет фактической реализации, его constructor_body состоит из точки с запятой. Для всех остальных конструкторов constructor_body состоит из обоих конструкторов.

  • блок, указывающий операторы для инициализации нового экземпляра класса; или
  • текст выражения, состоящий из =>выражения и точки с запятой, и обозначает одно выражение для инициализации нового экземпляра класса.

Constructor_body, который является блоком или текстом выражения, точно соответствует блоку метода экземпляра с типом void возвращаемого значения (§15.6.11).

Конструкторы экземпляров не наследуются. Таким образом, класс не имеет конструкторов экземпляров, отличных от фактически объявленных в классе, за исключением того, что если класс не содержит объявлений конструктора экземпляров, конструктор экземпляра по умолчанию автоматически предоставляется (§15.11.5).

Конструкторы экземпляров вызываются object_creation_expressions (§12.8.17.2) и constructor_initializer s.

Инициализаторы конструктора 15.11.2

Все конструкторы экземпляров (за исключением objectклассов) неявно включают вызов другого конструктора экземпляра непосредственно перед constructor_body. Конструктор для неявного вызова определяется constructor_initializer:

  • Инициализатор конструктора экземпляра формы base(argument_list) (где argument_list является необязательным) вызывает конструктор экземпляра из прямого базового класса. Этот конструктор выбирается с помощью argument_list и правил разрешения перегрузки в §12.6.4. Набор конструкторов экземпляров-кандидатов состоит из всех конструкторов доступных экземпляров прямого базового класса. Если этот набор пуст или если не удается определить один лучший конструктор экземпляра, возникает ошибка во время компиляции.
  • Инициализатор конструктора экземпляра формы this(argument_list) (где argument_list является необязательным) вызывает другой конструктор экземпляра из того же класса. Конструктор выбирается с помощью argument_list и правил разрешения перегрузки в §12.6.4. Набор конструкторов экземпляров-кандидатов состоит из всех конструкторов экземпляров, объявленных в самом классе. Если результирующий набор применимых конструкторов экземпляров пуст или если не удается определить один лучший конструктор экземпляра, возникает ошибка во время компиляции. Если объявление конструктора экземпляра вызывается через цепочку одного или нескольких инициализаторов конструкторов, возникает ошибка во время компиляции.

Если конструктор экземпляра не имеет инициализатора конструктора, инициализатор base() формы неявно предоставляется.

Примечание. Таким образом, объявление конструктора экземпляра формы

C(...) {...}

точно эквивалентен

C(...) : base() {...}

конечная заметка

Область параметров, заданных parameter_list объявления конструктора экземпляра, включает инициализатор конструктора этого объявления. Таким образом, инициализатор конструктора может получить доступ к параметрам конструктора.

Пример:

class A
{
    public A(int x, int y) {}
}

class B: A
{
    public B(int x, int y) : base(x + y, x - y) {}
}

пример конца

Инициализатор конструктора экземпляра не может получить доступ к созданному экземпляру. Поэтому это ошибка во время компиляции для ссылки на это в выражении аргумента инициализатора конструктора, так как это ошибка во время компиляции для выражения аргумента для ссылки на любой член экземпляра через simple_name.

Инициализаторы переменных экземпляра 15.11.3

Если конструктор экземпляра не имеет инициализатора конструктора или имеет инициализатор конструктора формы base(...), конструктор неявно выполняет инициализации, указанные variable_initializerполя экземпляра, объявленные в своем классе. Это соответствует последовательности назначений, которые выполняются сразу после входа в конструктор и перед неявным вызовом конструктора прямого базового класса. Инициализаторы переменных выполняются в текстовом порядке, в котором они отображаются в объявлении класса (§15.5.6).

Выполнение конструктора 15.11.4

Инициализаторы переменных преобразуются в инструкции присваивания, и эти инструкции присваиваются перед вызовом конструктора экземпляра базового класса. Это упорядочение гарантирует, что все поля экземпляра инициализированы их инициализаторами переменных перед выполнением любых инструкций, имеющих доступ к этому экземпляру.

Пример: учитывая следующее:

class A
{
    public A()
    {
        PrintFields();
    }

    public virtual void PrintFields() {}
}
class B: A
{
    int x = 1;
    int y;

    public B()
    {
        y = -1;
    }

    public override void PrintFields() =>
        Console.WriteLine($"x = {x}, y = {y}");
}

B() При создании экземпляра Bэкземпляра создается следующий результат:

x = 1, y = 0

Значение x равно 1, так как инициализатор переменных выполняется перед вызовом конструктора экземпляра базового класса. Однако значение y равно 0 (значение по умолчанию) intтак как назначение y не выполняется до тех пор, пока конструктор базового класса не возвращается. Полезно рассматривать инициализаторы переменных экземпляров и инициализаторы конструкторов как инструкции, автоматически вставляемые перед constructor_body. Пример

class A
{
    int x = 1, y = -1, count;

    public A()
    {
        count = 0;
    }

    public A(int n)
    {
        count = n;
    }
}

class B : A
{
    double sqrt2 = Math.Sqrt(2.0);
    ArrayList items = new ArrayList(100);
    int max;

    public B(): this(100)
    {
        items.Add("default");
    }

    public B(int n) : base(n - 1)
    {
        max = n;
    }
}

содержит несколько инициализаторов переменных; он также содержит инициализаторы конструкторов обеих форм (base и this). Пример соответствует приведенному ниже коду, где каждый комментарий указывает автоматически вставленный оператор (синтаксис, используемый для вызовов автоматически вставленного конструктора, не является допустимым, но просто служит для иллюстрации механизма).

class A
{
    int x, y, count;
    public A()
    {
        x = 1;      // Variable initializer
        y = -1;     // Variable initializer
        object();   // Invoke object() constructor
        count = 0;
    }

    public A(int n)
    {
        x = 1;      // Variable initializer
        y = -1;     // Variable initializer
        object();   // Invoke object() constructor
        count = n;
    }
}

class B : A
{
    double sqrt2;
    ArrayList items;
    int max;
    public B() : this(100)
    {
        B(100);                      // Invoke B(int) constructor
        items.Add("default");
    }

    public B(int n) : base(n - 1)
    {
        sqrt2 = Math.Sqrt(2.0);      // Variable initializer
        items = new ArrayList(100);  // Variable initializer
        A(n - 1);                    // Invoke A(int) constructor
        max = n;
    }
}

пример конца

Конструкторы по умолчанию 15.11.5

Если класс не содержит объявлений конструктора экземпляра, конструктор экземпляра по умолчанию автоматически предоставляется. Этот конструктор по умолчанию просто вызывает конструктор прямого базового класса, как если бы он имел инициализатор конструктора формы base(). Если класс абстрактен, то объявленная специальные возможности для конструктора по умолчанию защищена. В противном случае объявленный специальный доступ для конструктора по умолчанию является общедоступным.

Примечание. Таким образом, конструктор по умолчанию всегда имеет форму.

protected C(): base() {}

or

public C(): base() {}

где C — имя класса.

конечная заметка

Если разрешение перегрузки не может определить уникального кандидата для инициализатора конструктора базового класса, возникает ошибка во время компиляции.

Пример. В следующем коде

class Message
{
    object sender;
    string text;
}

Конструктор по умолчанию предоставляется, так как класс не содержит объявлений конструктора экземпляра. Таким образом, пример точно эквивалентен

class Message
{
    object sender;
    string text;

    public Message() : base() {}
}

пример конца

15.12 Статические конструкторы

Статический конструктор — это член, реализующий действия, необходимые для инициализации закрытого класса. Статические конструкторы объявляются с помощью static_constructor_declaration:

static_constructor_declaration
    : attributes? static_constructor_modifiers identifier '(' ')'
        static_constructor_body
    ;

static_constructor_modifiers
    : 'static'
    | 'static' 'extern' unsafe_modifier?
    | 'static' unsafe_modifier 'extern'?
    | 'extern' 'static' unsafe_modifier?
    | 'extern' unsafe_modifier 'static'
    | unsafe_modifier 'static' 'extern'?
    | unsafe_modifier 'extern' 'static'
    ;

static_constructor_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).

Static_constructor_declaration может включать набор атрибутов (§22) и модификатор (extern).

Идентификатор static_constructor_declaration должен называть класс, в котором объявлен статический конструктор. Если указано другое имя, возникает ошибка во время компиляции.

Если объявление статического конструктора включает extern модификатор, статический конструктор считается внешним статическим конструктором. Поскольку объявление внешнего статического конструктора не предоставляет фактической реализации, его static_constructor_body состоит из точки с запятой. Для всех других объявлений статических конструкторов static_constructor_body состоит из одного из следующих объявлений.

  • блок, указывающий инструкции для выполнения для инициализации класса; или
  • текст выражения, состоящий из =>выражения и точки с запятой, и обозначает одно выражение для инициализации класса.

Static_constructor_body, который является текстом блока или выражения, точно соответствует method_body статического метода с типом void возвращаемого значения (§15.6.11).

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

Статический конструктор для закрытого класса выполняется не более одного раза в определенном домене приложения. Выполнение статического конструктора активируется первым из следующих событий, которые происходят в домене приложения:

  • Создается экземпляр класса.
  • Ссылки на любые статические члены класса.

Если класс содержит Main метод (§7.1), в котором начинается выполнение, статический конструктор для этого класса выполняется перед вызовом Main метода.

Чтобы инициализировать новый тип закрытого класса, сначала создается новый набор статических полей (§15.5.2) для конкретного закрытого типа. Каждое из статических полей инициализировано в значение по умолчанию (§15.5.5). Затем для этих статических полей выполняются инициализаторы статических полей (§15.5.6.2). Наконец, выполняется статический конструктор.

Пример: пример

class Test
{
    static void Main()
    {
        A.F();
        B.F();
    }
}

class A
{
    static A()
    {
        Console.WriteLine("Init A");
    }

    public static void F()
    {
        Console.WriteLine("A.F");
    }
}

class B
{
    static B()
    {
        Console.WriteLine("Init B");
    }

    public static void F()
    {
        Console.WriteLine("B.F");
    }
}

должен производить выходные данные:

Init A
A.F
Init B
B.F

так как выполнение Aстатического конструктора активируется вызовом A.F, и выполнение Bстатического конструктора активируется вызовом B.F.

пример конца

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

Пример: пример

class A
{
    public static int X;

    static A()
    {
        X = B.Y + 1;
    }
}

class B
{
    public static int Y = A.X + 1;

    static B() {}

    static void Main()
    {
        Console.WriteLine($"X = {A.X}, Y = {B.Y}");
    }
}

выводятся следующие выходные данные

X = 1, Y = 2

Для выполнения Main метода система сначала запускает инициализатор до B.Yстатического конструктора класса B. YИнициализатор вызывает Aзапуск конструктора static , так как значение A.X ссылается. Статический конструкторA, в свою очередь, переходит к вычислению значения, и при этом извлекает значение XYпо умолчанию, равное нулю. A.X Таким образом, инициализировано до 1. Затем выполняется процесс Aинициализаторов статических полей и статического конструктора, возвращаясь к вычислению начального значения Y, результат которого становится 2.

пример конца

Так как статический конструктор выполняется ровно один раз для каждого закрытого типа класса, удобно применять проверки во время выполнения для параметра типа, который невозможно проверить во время компиляции с помощью ограничений (§15.2.5).

Пример. Следующий тип использует статический конструктор для принудительного применения аргумента типа в перечислении:

class Gen<T> where T : struct
{
    static Gen()
    {
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException("T must be an enum");
        }
    }
}

пример конца

15.13 Методы завершения

Примечание. В более ранней версии этой спецификации, то, что теперь называется "методом завершения", называется деструктором. Опыт показал, что термин "деструктор" вызвал путаницу и часто приводил к неправильным ожиданиям, особенно программистам, зная C++. В C++деструктор вызывается в детерминированном режиме, в то время как в C#метод завершения не является. Чтобы получить детерминированное поведение из C#, следует использовать Dispose. конечная заметка

Метод завершения является членом, который реализует действия для завершения существования экземпляра класса. Средство завершения объявляется с помощью finalizer_declaration:

finalizer_declaration
    : attributes? '~' identifier '(' ')' finalizer_body
    | attributes? 'extern' unsafe_modifier? '~' identifier '(' ')'
      finalizer_body
    | attributes? unsafe_modifier 'extern'? '~' identifier '(' ')'
      finalizer_body
    ;

finalizer_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).

Finalizer_declaration может включать набор атрибутов(§22).

Идентификатор finalizer_declarator должен называть класс, в котором объявлен метод завершения. Если указано другое имя, возникает ошибка во время компиляции.

Когда объявление метода завершения включает extern модификатор, метод завершения считается внешним методом завершения. Поскольку объявление внешнего средства завершения не предоставляет фактической реализации, его finalizer_body состоит из точки с запятой. Для всех остальных средств завершения finalizer_body состоит из любого из

  • блок, указывающий инструкции, выполняемые для завершения экземпляра класса.
  • или текст выражения, состоящий из =>выражения и точки с запятой, и обозначает одно выражение для выполнения для завершения экземпляра класса.

Finalizer_body, представляющий собой блок или текст выражения, соответствует точно method_body метода экземпляра с типом возвращаемого void значения (§15.6.11).

Методы завершения не наследуются. Таким образом, класс не имеет методов завершения, отличных от того, который может быть объявлен в этом классе.

Примечание. Так как метод завершения не требует параметров, его нельзя перегружать, поэтому класс может иметь, по крайней мере, один метод завершения. конечная заметка

Методы завершения вызываются автоматически и не могут вызываться явным образом. Экземпляр становится допустимым для завершения, если он больше не может использовать этот экземпляр. Выполнение средства завершения для экземпляра может произойти в любое время после того, как экземпляр станет допустимым для завершения (§7.9). После завершения экземпляра методы завершения в цепочке наследования экземпляра вызываются по порядку от большинства производных до наименее производных. Средство завершения может выполняться в любом потоке. Дополнительные сведения о правилах, определяющих, когда и как выполняется метод завершения, см. в разделе 7.9.

Пример: выходные данные примера

class A
{
    ~A()
    {
        Console.WriteLine("A's finalizer");
    }
}

class B : A
{
    ~B()
    {
        Console.WriteLine("B's finalizer");
    }
}

class Test
{
    static void Main()
    {
        B b = new B();
        b = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

-

B's finalizer
A's finalizer

так как методы завершения в цепочке наследования вызываются в порядке, от большинства производных до наименее производных.

пример конца

Методы завершения реализуются путем переопределения виртуального метода FinalizeSystem.Object. Программы C# не могут переопределить этот метод или вызвать его (или переопределения) напрямую.

Пример: например, программа

class A
{
    override protected void Finalize() {}  // Error
    public void F()
    {
        this.Finalize();                   // Error
    }
}

содержит две ошибки.

пример конца

Компилятор должен вести себя так, как если бы этот метод и его переопределения вообще не существовали.

Пример. Таким образом, эта программа:

class A
{
    void Finalize() {}  // Permitted
}

является допустимым и показанный метод скрывает System.ObjectFinalize метод.

пример конца

Обсуждение поведения при возникновении исключения из средства завершения см. в разделе "21.4".

15.14 Итераторы

15.14.1 Общие

Элемент функции (§12.6), реализованный с помощью блока итератора (§13.3) называется итератором.

Блок итератора может использоваться в качестве текста элемента функции, если возвращаемый тип соответствующего элемента функции является одним из интерфейсов перечислителя (§15.14.2) или одного из перечисленных интерфейсов (§15.14.3). Это может произойти как method_body, operator_body или accessor_body, в то время как события, конструкторы экземпляров, статические конструкторы и метод завершения не должны быть реализованы как итераторы.

Если член функции реализуется с помощью блока итератора, это ошибка во время компиляции для списка параметров члена функции, указывающего любой inэлемент функции, outили ref параметры или параметр ref struct типа.

Интерфейсы перечислителя 15.14.2

Интерфейсы System.Collections.IEnumerator универсального интерфейсаSystem.Collections.Generic.IEnumerator<T>. Для краткости в этом подклаузе и его братьях и сестрах эти интерфейсы ссылаются как IEnumerator и IEnumerator<T>соответственно.

Интерфейсы перечисления 15.14.3

Перечисляемые System.Collections.IEnumerable универсального интерфейсаSystem.Collections.Generic.IEnumerable<T>. Для краткости в этом подклаузе и его братьях и сестрах эти интерфейсы ссылаются как IEnumerable и IEnumerable<T>соответственно.

Тип доходности 15.14.4

Итератор создает последовательность значений всех одинаковых типов. Этот тип называется типом доходности итератора.

  • Тип доходности итератора, который возвращает IEnumerator или IEnumerable является object.
  • Тип доходности итератора, который возвращает IEnumerator<T> или IEnumerable<T> является T.

Объекты перечислителя 15.14.5

15.14.5.1 Общие

Если элемент функции, возвращающий тип интерфейса перечислителя, реализуется с помощью блока итератора, вызов элемента функции не сразу выполняет код в блоке итератора. Вместо этого создается и возвращается объект перечислителя. Этот объект инкапсулирует код, указанный в блоке итератора, и выполнение кода в блоке итератора происходит при вызове метода объекта MoveNext перечислителя. Объект перечислителя имеет следующие характеристики:

  • Он реализует и IEnumerator, IEnumerator<T> где T тип доходности итератора.
  • Он реализует System.IDisposable.
  • Он инициализирован с копией значений аргументов (если таковой) и значения экземпляра, передаваемые члену функции.
  • Он имеет четыре потенциальных состояния, до, запуск, приостановку и после, и первоначально находится в состоянии до.

Объект перечислителя обычно является экземпляром класса перечислителя, созданного компилятором, который инкапсулирует код в блоке итератора и реализует интерфейсы перечислителя, но возможны другие методы реализации. Если класс перечислителя создается компилятором, этот класс будет вложен напрямую или косвенно в класс, содержащий член функции, он будет иметь частные специальные возможности, и он будет иметь имя, зарезервированное для использования компилятором (§6.4.3).

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

В следующих подклаузах описывается требуемое поведение MoveNextи CurrentDispose члены IEnumeratorIEnumerator<T> реализаций интерфейса, предоставляемых объектом перечислителя.

Объекты перечислителя не поддерживают IEnumerator.Reset метод. Вызов этого метода приводит System.NotSupportedException к возникновению ошибки.

15.14.5.2 Метод MoveNext

Метод MoveNext объекта перечислителя инкапсулирует код блока итератора. Вызов MoveNext метода выполняет код в блоке итератора и задает Current свойство объекта перечислителя соответствующим образом. Точное действие, выполняемое в MoveNext зависимости от состояния объекта перечислителя при MoveNext вызове:

  • Если состояние объекта перечислителя до этого, вызовитеMoveNext:
    • Изменяет состояние выполнения.
    • Инициализирует параметры блока thisитератора в значения аргументов и значение экземпляра, сохраненные при инициализации объекта перечислителя.
    • Выполняет блок итератора от начала до прерывания выполнения (как описано ниже).
  • Если выполняется состояние объекта перечислителя, результат вызова MoveNext не указан.
  • Если состояние объекта перечислителя приостановлено, вызов moveNext:
    • Изменяет состояние выполнения.
    • Восстанавливает значения всех локальных переменных и параметров (включая this) значения, сохраненные при выполнении блока итератора, в последний раз приостановлены.

      Примечание. Содержимое любых объектов, на которые ссылается эти переменные, может измениться с момента предыдущего вызова MoveNext. конечная заметка

    • Возобновляет выполнение блока итератора сразу после инструкции возвращаемого значения, которая вызвала приостановку выполнения и продолжается до прерывания выполнения (как описано ниже).
  • Если состояние объекта перечислителя находится после, вызов MoveNext возвращает значение false.

При MoveNext выполнении блока итератора выполнение может быть прервано четырьмя способами: yield return оператором, yield break оператором, путем обнаружения конца блока итератора, а также путем исключения, вызываемого и распространяемого из блока итератора.

  • yield return При обнаружении оператора (§9.4.4.20):
    • Выражение, заданное в инструкции, вычисляется, неявно преобразуется в тип доходности и назначается Current свойству объекта перечислителя.
    • Выполнение текста итератора приостановлено. Значения всех локальных переменных и параметров (включая this) сохраняются, как расположение этой yield return инструкции. yield return Если оператор находится в одном или нескольких try блоках, связанные блоки, в настоящее время не выполняются связанные блоки.
    • Состояние объекта перечислителя изменяется на приостановленное.
    • Метод MoveNext возвращается true вызывающему объекту, указывая, что итерация успешно расширена до следующего значения.
  • yield break При обнаружении оператора (§9.4.4.20):
    • yield break Если инструкция находится в одном или нескольких try блоках, выполняются связанные finally блоки.
    • Состояние объекта перечислителя изменяется на после.
    • Метод MoveNext возвращается false вызывающему объекту, указывая, что итерация завершена.
  • При обнаружении конца текста итератора:
    • Состояние объекта перечислителя изменяется на после.
    • Метод MoveNext возвращается false вызывающему объекту, указывая, что итерация завершена.
  • При возникновении исключения и распространении из блока итератора:
    • Соответствующие finally блоки в тексте итератора будут выполняться распространением исключений.
    • Состояние объекта перечислителя изменяется на после.
    • Распространение исключений продолжается вызывающим методом MoveNext .

15.14.5.3 Текущее свойство

Свойство объекта Current перечислителя влияет на yield return операторы в блоке итератора.

Если объект перечислителя находится в приостановленном состоянии, значением является значение Current , заданное предыдущим вызовом MoveNext. Если объект перечислителя находится в состоянии до, выполнения или после состояния, результат доступа Current не указан.

Для итератора с типом доходности, кроме objectтого, результат доступа Current через реализацию объекта IEnumerable перечислителя соответствует реализации Current перечислителя через реализацию объекта IEnumerator<T> перечислителя и приведение результата к objectнему.

15.14.5.4 Метод Dispose

Метод Dispose используется для очистки итерации путем добавления объекта перечислителя в состояние после .

  • Если состояние объекта перечислителя до этого, вызов Dispose состояния изменится на после.
  • Если выполняется состояние объекта перечислителя, результат вызова Dispose не указан.
  • Если состояние объекта перечислителя приостановлено, вызовите Dispose:
    • Изменяет состояние выполнения.
    • Выполняет все блоки, наконец, как если бы последняя выполненная yield return инструкция была оператором yield break . Если это приводит к возникновению и распространению исключения из текста итератора, состояние объекта перечислителя устанавливается после и исключение распространяется вызывающему Dispose методу.
    • Изменяет состояние после.
  • Если состояние объекта перечислителя находится после, вызов Dispose не влияет.

Объекты перечисления 15.14.6

15.14.6.1 General

Если элемент функции, возвращающий тип перечисленного интерфейса, реализуется с помощью блока итератора, вызов элемента функции не сразу выполняет код в блоке итератора. Вместо этого создается и возвращается перечислимый объект . Метод перечисленного объекта GetEnumerator возвращает объект перечислителя, который инкапсулирует код, указанный в блоке итератора, и выполнение кода в блоке итератора происходит при MoveNext вызове метода перечислителя. Перечисленный объект имеет следующие характеристики:

  • Он реализует и IEnumerable, IEnumerable<T> где T тип доходности итератора.
  • Он инициализирован с копией значений аргументов (если таковой) и значения экземпляра, передаваемые члену функции.

Перечисляемый объект обычно является экземпляром класса, созданного компилятором, который инкапсулирует код в блоке итератора и реализует перечисляемые интерфейсы, но возможны другие методы реализации. Если перечисленный класс создается компилятором, этот класс будет вложен напрямую или косвенно в класс, содержащий член функции, он будет иметь частные специальные возможности, и он будет иметь имя, зарезервированное для использования компилятором (§6.4.3).

Перечисленный объект может реализовать больше интерфейсов, чем указанные выше.

Примечание. Например, перечислимый объект также может реализовываться IEnumerator и IEnumerator<T>, что позволяет использовать его как перечисление, так и перечислитель. Как правило, такая реализация возвращает собственный экземпляр (для сохранения выделений) из первого вызова GetEnumerator. Последующие вызовы GetEnumerator, если таковые есть, возвращают новый экземпляр класса, как правило, одного класса, чтобы вызовы разных экземпляров перечислителя не влияли друг на друга. Он не может возвращать тот же экземпляр, даже если предыдущий перечислитель уже перечислил после конца последовательности, так как все будущие вызовы исчерпанного перечислителя должны вызывать исключения. конечная заметка

15.14.6.2 Метод GetEnumerator

Перечисляемый объект предоставляет реализацию GetEnumerator методов IEnumerable и IEnumerable<T> интерфейсов. Два GetEnumerator метода совместно используют общую реализацию, которая получает и возвращает доступный объект перечислителя. Объект перечислителя инициализируется со значениями аргументов и значением экземпляра, сохраненным при инициализации объекта перечисления, но в противном случае объект перечислителя работает, как описано в разделе §15.14.5.

15.15 Асинхронные функции

15.15.1 General

Метод (§15.6) или анонимная функция (§12.19) с модификатором называется асинхронной async. Как правило, асинхронный термин используется для описания любой функции, которая имеет async модификатор.

Это ошибка во время компиляции для списка параметров асинхронной функции, чтобы указать любой inout, или ref параметры или любой ref struct параметр типа.

Return_type асинхронного метода должен быть либо void задачи. Для асинхронного метода, создающего значение результата, тип задачи должен быть универсальным. Для асинхронного метода, который не создает результирующий значение, тип задачи не должен быть универсальным. Такие типы называются в этой спецификации «TaskType»<T> и «TaskType»соответственно. Тип System.Threading.Tasks.Task и типы стандартной библиотеки, созданные из System.Threading.Tasks.Task<TResult> них, являются типами задач, а также классом, структурой или типом интерфейса, связанным с типом построителя задач с помощью атрибута.System.Runtime.CompilerServices.AsyncMethodBuilderAttribute Такие типы называются в этой спецификации и «TaskBuilderType»<T>«TaskBuilderType». Тип задачи может иметь не более одного параметра типа и не может быть вложен в универсальный тип.

Асинхронный метод, возвращающий тип задачи, считается возвращающим задачу.

Типы задач могут отличаться в их точном определении, но с точки зрения языка тип задачи находится в одном из состояний , неполных, успешных или неисправных. Сбой задачи записывает соответствующее исключение. Успешно записывает«TaskType»<T> результат типа T. Типы задач являются ожидаемыми, поэтому задачи могут быть операнды выражений await (§12.9.8).

Пример. Тип MyTask<T> задачи связан с типом MyTaskMethodBuilder<T> построителя задач и типом Awaiter<T>ожидания:

using System.Runtime.CompilerServices; 
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
class MyTask<T>
{
    public Awaiter<T> GetAwaiter() { ... }
}

class Awaiter<T> : INotifyCompletion
{
    public void OnCompleted(Action completion) { ... }
    public bool IsCompleted { get; }
    public T GetResult() { ... }
}

пример конца

Тип построителя задач — это класс или тип структуры, соответствующий определенному типу задачи (§15.15.2). Тип построителя задач должен точно соответствовать объявленной доступности соответствующего типа задачи.

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

Асинхронная функция имеет возможность приостановить оценку с помощью выражений await (§12.9.8) в тексте. Позже оценка может быть возобновлена в момент приостановки выражения ожидания с помощью делегата возобновления. Делегат возобновления имеет тип System.Action, и при его вызове оценка вызова асинхронной функции возобновляется из выражения await, в котором оно осталось. Текущий вызывающий вызов асинхронной функции — это исходный вызывающий объект, если вызов функции никогда не был приостановлен или последний вызывающий делегат возобновления в противном случае.

Шаблон построителя типов задач 15.15.2

Тип построителя задач может иметь не более одного параметра типа и не может быть вложен в универсальный тип. Тип построителя задач должен иметь следующие члены (для типов построителя задач, SetResult не имеющих параметров) с объявленными public специальными возможностями:

class «TaskBuilderType»<T>
{
    public static «TaskBuilderType»<T> Create();
    public void Start<TStateMachine>(ref TStateMachine stateMachine)
                where TStateMachine : IAsyncStateMachine;
    public void SetStateMachine(IAsyncStateMachine stateMachine);
    public void SetException(Exception exception);
    public void SetResult(T result);
    public void AwaitOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine;
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine;
    public «TaskType»<T> Task { get; }
}

Компилятор должен создать код, использующий TaskBuilderType для реализации семантики приостановки и возобновления оценки асинхронной функции. Компилятор должен использовать taskBuilderType следующим образом:

  • «TaskBuilderType».Create() вызывается для создания экземпляра taskBuilderType, названного builder в этом списке.
  • builder.Start(ref stateMachine) вызывается для связывания построителя с экземпляром stateMachineкомпьютера, созданного компилятором, .
    • Построитель должен вызвать stateMachine.MoveNext() либо в Start() , либо после Start() того, как он вернулся для продвижения государственного компьютера.
  • После Start() возврата async метод вызывает builder.Task задачу, возвращаемую из асинхронного метода.
  • Каждый вызов будет stateMachine.MoveNext() продвигать компьютер состояния.
  • Если компьютер состояния завершается успешно, builder.SetResult() вызывается с возвращаемым значением метода, если таковой имеется.
  • В противном случае, если исключение e создается на компьютере состояния, builder.SetException(e) вызывается.
  • Если компьютер состояния достигает await expr выражения, expr.GetAwaiter() вызывается.
  • Если средство ожидания реализует ICriticalNotifyCompletion и IsCompleted имеет значение false, то вызывается builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)компьютер состояния.
    • AwaitUnsafeOnCompleted() должен вызываться awaiter.UnsafeOnCompleted(action) с вызовом Action , который вызывается stateMachine.MoveNext() после завершения ожидания.
  • В противном случае вызывается компьютер builder.AwaitOnCompleted(ref awaiter, ref stateMachine)состояния.
    • AwaitOnCompleted() должен вызываться awaiter.OnCompleted(action) с вызовом Action , который вызывается stateMachine.MoveNext() после завершения ожидания.
  • SetStateMachine(IAsyncStateMachine) Может вызываться реализацией, созданной IAsyncStateMachine компилятором, чтобы определить экземпляр построителя, связанного с экземпляром компьютера состояния, особенно в тех случаях, когда конечный компьютер реализуется как тип значения.
    • Если построитель вызывает, вызовет stateMachine.SetStateMachine(stateMachine)stateMachine экземпляр построителя, builder.SetStateMachine(stateMachine) связанный сstateMachine.

Примечание. Для обоих SetResult(T result) и «TaskType»<T> Task { get; }, соответственно, параметр и аргумент должны быть преобразованы Tв идентификатор. Это позволяет построителю типов задач поддерживать такие типы, как кортежи, в которых два типа, которые не одинаковы, являются преобразованными удостоверениями. конечная заметка

15.15.3. Оценка возвращаемой задачи асинхронной функции

Вызов возвращаемой задачи асинхронной функции приводит к созданию экземпляра возвращаемого типа задачи. Это называется задачей возврата асинхронной функции. Задача изначально находится в неполном состоянии.

Затем текст асинхронной функции вычисляется до тех пор, пока он не будет приостановлен (путем достижения выражения await) или завершается, когда элемент управления точками возвращается вызывающему объекту вместе с задачей возврата.

Когда текст асинхронной функции завершается, задача возврата перемещается из неполного состояния:

  • Если тело функции завершается в результате достижения инструкции возврата или конца тела, любое значение результата записывается в задаче возврата, которая помещается в состояние успешного выполнения.
  • Если тело функции завершается из-за неученного OperationCanceledException, исключение записывается в задаче возврата, которая помещается в отмененное состояние.
  • Если тело функции завершается в результате любого другого неухваченного исключения (§13.10.6), исключение записывается в задаче возврата, которая помещается в состояние сбоя .

15.15.4 Оценка асинхронной функции, возвращающей пустоту

Если возвращаемый тип асинхронной функции voidявляется, оценка отличается от приведенного выше способа: поскольку задача не возвращается, функция вместо этого сообщает о завершении и исключениях в контексте синхронизации текущего потока. Точное определение контекста синхронизации зависит от реализации, но представляет собой представление "где" текущий поток выполняется. Контекст синхронизации уведомляется при оценке voidвозвращающей асинхронной функции, успешно завершается или вызывает неуверенное исключение.

Это позволяет контексту отслеживать, сколько voidасинхронных функций выполняется под ним, и решить, как распространять исключения, исходящие из них.