Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
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 состоит из необязательного набора атрибутов (§22), за которыми следует необязательный набор class_modifier (§15.2.2), затем необязательный модификатор (§15.2.7), за которым идет ключевое слово и идентификатор, который называет класс, затем необязательный 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
, protected
, internal
и private
управляют доступностью класса. В зависимости от контекста, в котором происходит объявление класса, некоторые из этих модификаторов могут быть запрещены (§7.5.2).
Если объявление частичного типа (§15.2.7) включает спецификацию доступа (с помощью модификаторов , public
, protected
, и internal
), эта спецификация должна соответствовать всем другим частям, включающим спецификацию доступа. Если часть частичного типа не включает спецификацию специальных возможностей, тип получает соответствующую доступность по умолчанию (§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
используется для предотвращения производных от класса. Ошибка во время компиляции возникает, если запечатанный класс указан как базовый класс другого класса.
Запечатанный класс также не может быть абстрактным классом.
Примечание: модификатор в основном используется для предотвращения непреднамеренного наследования, но также обеспечивает определенные оптимизации во время выполнения. В частности, поскольку запечатанный класс по определению никогда не имеет производных классов, можно преобразовать вызовы виртуальных функций на экземплярах запечатанных классов в невиртуальные вызовы. конечная заметка
Если одна или несколько частей объявления частичного типа (§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)
.
Основное_выражение (§12.8) может ссылаться на статический класс, если
- Основное выражение
является в доступе к члену ( §12.8.7 ) формы.
В любом другом контексте это ошибка компиляции при обращении к статическому классу.
Примечание. Например, это ошибка для статического класса, используемого в качестве базового класса, составного типа (§15.3.7) элемента, аргумента универсального типа или ограничения параметра типа. Аналогичным образом статический класс нельзя использовать в типе массива, новом выражении, выражении приведения, выражении is, выражении as, выражении
sizeof
или выражении значения по умолчанию. конечная заметка
Параметры типа 15.2.3
Параметр типа — это простой идентификатор, обозначающий заполнитель для аргумента типа, предоставленного для создания созданного типа. Напротив, аргумент типа (§8.4.2) — это тип, который заменяет параметр типа при создании построенного типа.
type_parameter_list
: '<' decorated_type_parameter (',' decorated_type_parameter)* '>'
;
decorated_type_parameter
: 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.Delegate
System.Enum
System.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
(его прямого базового класса), который зависит отB
(его непосредственно заключающего класса), от которого циклически зависитA
.конечный пример
Класс не зависит от классов, вложенных в него.
Пример. В следующем коде
class A { class B : A {} }
B
зависит отA
(посколькуA
является как его прямым базовым классом, так и немедленно окружающим классом), ноA
не зависит отB
(так какB
не является ни базовым, ни окружающим классом для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 {...}
Набор базовых интерфейсов для класса
C
—IA
,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_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()
Основное ограничение может быть типом класса, ограничением типа ссылки, ограничением типа значения, ограничением на не-null или ограничением на неуправляемый тип. Тип класса и ограничение ссылочного типа могут включать nullable_type_annotation.
Дополнительное ограничение может быть «interface_type» или «type_parameter», за которыми, возможно, следует аннотация на отсутствие значения «nullable_type_annotation». Наличие nullable_type_annotation указывает, что аргумент типа может быть ссылочным типом, допускающим значение NULL, который соответствует ненулевому типу ссылок, удовлетворяющему ограничению.
Ограничение ссылочного типа указывает, что аргумент типа, используемый для параметра типа, должен быть ссылочным типом. Все типы классов, типы интерфейсов, типы делегатов, типы массивов и параметры типа, известные как ссылочный тип (как определено ниже), удовлетворяют этому ограничению.
Тип класса, ограничение ссылочного типа и вторичные ограничения могут включать аннотацию Nullable. Наличие или отсутствие этой аннотации у параметра типа указывает на ожидания в отношении допустимости null для аргумента типа.
- Если ограничение не включает аннотацию nullable типа, аргумент типа должен быть ссылочным типом, не допускающим значение NULL. Компилятор может выдавать предупреждение, если аргумент типа является ссылочным типом, допускающим значение NULL.
- Если ограничение включает заметку типа NULL, ограничение удовлетворяется как типом ссылок, не допускающим значение NULL, так и ссылочным типом, допускающим значение NULL.
Допустимость null для аргумента типа не обязательно должна совпадать с допустимостью null для параметра типа. Компилятор может выдавать предупреждение, если значение NULL параметра типа не соответствует значению NULL аргумента типа.
Примечание. Чтобы указать, что аргумент типа является ссылочным типом, допускающим значение NULL, не добавляйте заметку типа NULL в качестве ограничения (используйте
T : class
илиT : BaseClass
), но используйтеT?
в универсальном объявлении для указания соответствующего ссылочного типа, допускающего значение 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 указывает, что аргумент типа, используемый для параметра типа, должен быть ненулевым значимым типом или ненулевым ссылочным типом. Аргумент типа, который не является ненулевым типом значения или ненулевым ссылочным типом, допускается, но компилятор может выдать диагностическое предупреждение.
Так как notnull
не является ключевым словом, в primary_constraint ограничение NOT NULL всегда синтаксически неоднозначно по отношению к class_type. По соображениям совместимости, если подстановка имени (§12.8.4) для имени notnull
завершится успешно, ее следует рассматривать как class_type
. В противном случае оно должно рассматриваться как ненульное ограничение.
Пример. Следующий класс демонстрирует использование различных аргументов типа для различных ограничений, указывающих предупреждения, которые могут выдаваться компилятором.
#nullable enable public class C { } public class A<T> where T : notnull { } public class B1<T> where T : C { } public class B2<T> where T : C? { } class Test { static void M() { // nonnull constraint allows nonnullable struct type argument A<int> x1; // possible warning: nonnull constraint prohibits nullable struct type argument A<int?> x2; // nonnull constraint allows nonnullable class type argument A<C> x3; // possible warning: nonnull constraint prohibits nullable class type argument A<C?> x4; // nonnullable base class requirement allows nonnullable class type argument B1<C> x5; // possible warning: nonnullable base class requirement prohibits nullable class type argument B1<C?> x6; // nullable base class requirement allows nonnullable class type argument B2<C> x7; // nullable base class requirement allows nullable class type argument B2<C?> x8; } }
Ограничение типа значения указывает, что аргумент типа, используемый для параметра типа, должен быть типом значения, не допускающим значение NULL. Все типы структур, не допускающие значения NULL, типы перечисления и параметры типа, имеющие ограничение типа значения, удовлетворяют этому ограничению. Обратите внимание, что хотя классифицируется как тип значения, тип значения null (§8.3.12) не удовлетворяет ограничению типа значения. Параметр типа, имеющий ограничение типа значения, не может также иметь constructor_constraint, хотя его можно использовать как аргумент типа для другого параметра типа с constructor_constraint.
Примечание. Тип
System.Nullable<T>
задает ограничение типа значения, не допускающего значение NULL, дляT
. Таким образом, рекурсивно построенные типы формT??
иNullable<Nullable<T>>
запрещены. конечная заметка
Ограничение неуправляемого типа указывает, что аргумент типа, используемый для параметра типа, должен быть неуправляемым типом (§8.8).
Поскольку unmanaged
это не ключевое слово, в primary_constraint неуправляемое ограничение всегда синтаксически неоднозначно с class_type. По соображениям совместимости, если поиск имени (§12.8.4) имени unmanaged
проходит успешно, оно рассматривается как class_type
. В противном случае оно рассматривается как неуправляемые ограничения.
Типы указателей никогда не могут быть аргументами типа и не удовлетворяют ни одному ограничению типа, даже неуправляемому, несмотря на неуправляемые типы.
Если ограничение является типом класса, типом интерфейса или параметром типа, этот тип указывает минимальный базовый тип, который должен поддерживать каждый аргумент типа, используемый для этого параметра типа. При использовании созданного типа или универсального метода аргумент типа проверяется на соответствие ограничениям параметра типа во время компиляции. Указанный аргумент типа должен соответствовать условиям, описанным в разделе 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
зависит от параметраU
типа, тоS
зависит отU
.
Учитывая это отношение, ошибка компиляции возникает, если параметр типа зависит от себя (прямо или косвенно).
Все ограничения должны быть согласованы между зависимыми параметрами типа. Если параметр S
типа зависит от параметра T
типа, то:
-
T
не должно иметь ограничения типа значения. В противном случаеT
по существу закрыт, поэтомуS
должен быть таким же типом, какT
, устраняя необходимость двух параметров типа. - Если
имеет ограничение типа значения, то не должно иметь ограничение типа класса . - Если у
S
есть ограничение class_typeA
, и уT
есть ограничение class_typeB
, то должно выполняться преобразование идентичности или неявное преобразование ссылки изA
вB
, или неявное преобразование ссылки изB
вA
. - Если
S
также зависит от параметра типаU
, иU
имеет ограничение типа_класса , иA
имеет ограничение типа_классаT
, то должно быть преобразование идентичности или неявное преобразование ссылок из вB
, или неявное преобразование ссылок изA
вB
.
Допустимо, чтобы у S
было ограничение типа значения, а у T
— ограничение ссылочного типа. Фактически это ограничивает T
типами System.Object
, System.ValueType
, System.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ₓ
. - Если
C
Cₓ
является созданным типом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
. (Частичный тип может содержать объявления частичных методов (§15.6.9).
Каждая часть объявления частичного типа должна включать 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, вводят новые элементы в это пространство объявлений. Следующие правила применяются к объявлениям_членов_класса:
Конструкторы экземпляров, финализаторы и статические конструкторы должны иметь то же имя, что и непосредственно окружающий класс. Все остальные члены должны иметь имена, отличающиеся от имени непосредственно окружающего класса.
Имя параметра типа в 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 void M(); // Ok, defining partial method declaration partial class Inner // Ok, Inner is a partial type { int y; } } partial class A { int x; // Error, cannot declare x more than once partial void M() { } // Ok, implementing partial method declaration 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 Члены конструированных типов
Неунаследованные члены конструированного типа создаются путём подстановки для каждого параметра типа в объявлении члена соответствующего аргумента типа конструированного типа. Процесс замещения основан на семантическом значении объявлений типов, а не является простой подстановкой текста.
Пример: При условии объявления обобщенного класса
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>
имеет публичный членint
G(string s)
, который является ненаследуемым и получен путем замены аргумента типаint
на параметр типаT
.D<int>
также имеет унаследованный член от классаB
. Этот унаследованный элемент определяется путем сначала установления типа базового классаB<int[]>
D<int>
с заменойint
наT
в спецификации базового классаB<T[]>
. Затем в качестве аргумента типа дляB
используетсяint[]
, заменены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 protected
, internal
, или 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) имеет собственный набор статических полей независимо от количества экземпляров закрытого созданного типа.
- Статический член класса (метод, свойство, событие, оператор или конструктор) не работает с конкретным экземпляром, и обращение к 'this' в таком члене класса является ошибкой во время компиляции.
Если объявление поля, метода, свойства, события, индексатора, конструктора или финализатора не включает статический модификатор, оно объявляет член экземпляра. (Иногда элемент экземпляра называется нестатичным элементом.) Члены экземпляра имеют следующие характеристики:
- Если элемент
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
или internal
объявленную доступность и по умолчанию имеют internal
объявленную доступность. Вложенные типы также могут иметь эти формы объявленной специальных возможностей, а также одну или несколько дополнительных форм объявленной специальных возможностей в зависимости от того, является ли содержащий тип классом или структурой:
- Вложенный тип, объявленный в классе, может иметь любой из разрешенных уровней доступа и, как и другие члены класса, по умолчанию имеет такой же уровень доступа.
- Вложенный тип, объявленный в структуре, может иметь любую из трех форм объявленной доступности (
public
,internal
илиprivate
) и по умолчанию, как и у других членов структуры, имеет доступность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.4), и их можно скрыть с помощью модификатора (§15.3.5).
Примечание. Резервирование этих имен служит трем целям:
- Чтобы разрешить базовой реализации использовать обычный идентификатор в качестве имени метода для получения или задания доступа к функции языка C#.
- Чтобы разрешить другим языкам взаимодействовать с использованием обычного идентификатора в качестве имени метода для получения или задания доступа к функции языка C#.
- Чтобы убедиться, что источник, принятый одним соответствующим компилятором, принимается другим, делая особенности зарезервированных имен элементов согласованными во всех реализациях 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'
;
Описание константы static
модификатор. В объявлении константы является ошибкой, если один и тот же модификатор появляется несколько раз.
Тип constant_declaration указывает тип членов, введённых этим объявлением. За типом следует список constant_declarator s (§13.6.3), каждый из которыхпредставляет новый элемент.
Constant_declarator состоит из идентификатора, который называет элемент, следуемого маркером "=
", за которым следует константа_выражение (§12.23), которое задает значение элемента.
Тип, указанный в объявлении константы, должен быть , sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
, bool
, string
или 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 case
операторы, объявления членовenum
, атрибуты и другие объявления констант. конечная заметка
Примечание. Как описано в §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.5), допустимое сочетание четырех модификаторов доступа (§15.3.6) и static
модификатор (§15.5.2). Кроме того, field_declaration может включать readonly
модификатор (§15.5.3) или volatile
модификатор (§15.5.4), но не оба одновременно. Атрибуты и модификаторы применяются ко всем элементам, объявленным field_declaration. Это ошибка, когда один и тот же модификатор появляется несколько раз в field_declaration.
Тип field_declaration определяет тип элементов, указанных в объявлении. За каждым типом следует список variable_declarators, каждый из которых добавляет нового члена. Variable_declarator состоит из идентификатора, который именует этот член, и дополнительно может включать маркер " " и инициализатор переменной (§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
, ,White
Red
Green
и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). Эти оптимизации могут выполняться компилятором, системой выполнения или оборудованием. Для переменных полей такие оптимизации переупорядочения ограничены:
- Чтение изменяющегося поля называется переменным чтением. Операция чтения volatile обладает семантикой 'acquire'; т. е. гарантируется, что она произойдет до любых обращений к памяти, которые произошли после нее в последовательности команд.
- Запись изменяющегося поля называется переменной записью. Volatile запись имеет "релизную семантику"; гарантируется, что это произойдет после всех обращений к памяти перед инструкцией записи в последовательности команд.
Эти ограничения гарантируют, что все потоки будут видеть volatile-записи, выполняемые любым другим потоком, в порядке выполнения. Соответствующая реализация не обязана предоставлять единого общего порядка записей volatile, как видно из всех потоков выполнения. Тип изменяющегося поля должен быть одним из следующих:
- Тип ссылки.
- Параметр_типа, который, как известно, является ссылочным типом (§15.2.5).
- Тип
byte
,sbyte
,short
,ushort
,int
,uint
,char
,float
,bool
,System.IntPtr
илиSystem.UIntPtr
. - Перечисление типа с базовым типом ,
byte
,sbyte
,short
,ushort
,int
или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
была видна основному потоку после записи в , и, следовательно, основной поток мог бы считать значение 0 из поляfinished
. Объявление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 Общие
Объявления полей могут включать инициализаторы переменных. Для статических полей инициализаторы переменных соответствуют операторам присваивания, выполняемым во время инициализации класса. Например, инициализаторы полей соответствуют операторам присваивания, выполняемым при создании экземпляра класса.
Пример: пример
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
, а такжеb
инициализируются значением0
(значение по умолчанию дляint
) до выполнения их инициализаторов. При исполнении инициализатораa
значениеb
, равно нулю, аa
инициализируется в1
. Когда выполняется инициализатор дляb
, значение a уже равно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'
| 'readonly' // direct struct members only
| 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 ';'
| ';'
;
Заметки грамматики:
- unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).
- при распознавании method_body, если применимы как null_conditional_invocation_expression, так и expression, следует выбрать первое.
Примечание. Перекрытие и приоритет между альтернативами здесь исключительно для описательного удобства; правила грамматики можно разработать, чтобы удалить перекрытие. ANTLR и другие системы грамматики принимают то же удобство и поэтому method_body автоматически имеет указанную семантику. конечная заметка
Method_declaration может включать набор атрибутов (§22) и один из разрешенных видов объявленной доступности (§15.3.6), new
(§15.3.5), static
(§15.6.3), virtual
(§15.6.4), override
(§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7), extern
(§15.6.8) и async
(§15.14). Кроме того, method_declaration, который содержится непосредственно в struct_declaration, может включать readonly
модификатор (§16.4.12).
Объявление имеет допустимое сочетание модификаторов, если справедливы все из следующих условий:
- Объявление включает допустимое сочетание модификаторов доступа (§15.3.6).
- Объявление не включает один и тот же модификатор несколько раз.
- Объявление включает в себя не более одного из следующих модификаторов:
static
,virtual
иoverride
. - Объявление включает в себя не более одного из следующих модификаторов:
new
иoverride
. - Если объявление включает
abstract
модификатор, объявление не содержит ни одного из следующих модификаторов:static
, ,virtual
sealed
илиextern
. - Если объявление включает
private
модификатор, объявление не содержит ни одного из следующих модификаторов:virtual
,override
илиabstract
. - Если объявление включает
sealed
модификатор, то объявление также включаетoverride
модификатор. - Если объявление включает модификатор
partial
, то оно не включает ни один из следующих модификаторов:new
,public
,protected
,internal
,private
,virtual
,sealed
,override
,abstract
илиextern
.
Методы классифицируются в зависимости от того, что они возвращают, если возвращают что-либо.
- Если присутствует
ref
, метод возвращается по ссылке и возвращает ссылку на переменную, которая может быть доступна только для чтения; - В противном случае, если return_type является
void
, метод не возвращает значение и не возвращает значение; - В противном случае метод возвращает по значению и выдает значение.
return_type объявления метода с возвратом значения или без возврата значения указывает тип результата, возвращаемого методом, при наличии такового. Только метод, не возвращающий значение, может включать partial
модификатор (§15.6.9). Если объявление включает async
модификатор, то return_type должен быть void
или метод возвращает по значению, а возвращаемый тип — тип задачи (§15.14.1).
Ref_return_type объявления метода return-by-ref указывает тип переменной, на которую ссылается variable_reference, возвращаемой методом.
Универсальный метод — это метод, объявление которого включает type_parameter_list. Это указывает параметры типа для метода. Необязательные type_parameter_constraints_clauseуказывают ограничения для параметров типа.
Универсальный method_declaration для явной реализации элемента интерфейса не должен иметь никаких ограничений параметров типа; объявление наследует любые ограничения от ограничений метода интерфейса.
Аналогичным образом, объявление метода с 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 метода с возвратом по ссылке может быть представлено точкой с запятой, блоком или выражением. Тело блока состоит из блока, который указывает инструкции для выполнения при вызове метода. Тело выражения состоит из =>
, за которым следует ref
, variable_reference и точка с запятой, и указывает на единственный variable_reference, который следует оценить при вызове метода.
Для абстрактных и экстерн-методов ref_method_body состоит просто из точки с запятой; для всех остальных методов ref_method_body является либо блоком, либо телом выражения.
Имя, число параметров типа и список параметров метода определяют подпись (§7.6) метода. В частности, сигнатура метода состоит из его имени, числа параметров типа и числа, parameter_mode_modifier s (§15.6.2.1) итипов его параметров. Возвращаемый тип не является частью сигнатуры метода, как и имена параметров, имена параметров типа или ограничения. Если тип параметра ссылается на параметр типа метода, порядковая позиция параметра типа (а не имя параметра типа) используется для эквивалентности типов.
Имя метода должно отличаться от имен всех других не-методов, объявленных в одном классе. Кроме того, подпись метода должна отличаться от сигнатур всех других методов, объявленных в одном классе, и два метода, объявленные в одном классе, не должны иметь сигнатуры, которые отличаются исключительно по in
, out
и ref
.
Типовые параметры метода находятся в области видимости в 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.
this
обозначает метод как метод расширения и допускается только для первого параметра статического метода в не обобщённом, не вложенном статическом классе. Если параметр является типом struct
или параметром типа, ограниченным к struct
, модификатор this
может сочетаться с модификатором ref
или модификатором in
, но не с модификатором out
. Методы расширения описаны далее в разделе §15.6.10.
fixed_parameter с default_argument называется необязательным параметром, а fixed_parameter без default_argument является обязательным параметром. Обязательный параметр не должен отображаться после необязательного параметра в parameter_list.
Параметр с модификатором ref
, out
или this
не может иметь значение по умолчанию default_argument. Входной параметр может иметь default_argument.
Выражение в default_argument должно быть одним из следующих:
- константное_выражение
- выражение формы
new S()
, гдеS
является типом значения - выражение формы
default(S)
, гдеS
является типом значения
Выражение должно быть неявно преобразовано идентификационным преобразованием или преобразованием с null-значением к типу параметра.
Если необязательные параметры указаны в объявлении частичного метода (§15.6.9), явной реализации члена интерфейса (§18.6.2), объявлении индексатора с одним параметром (§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
,s
иo
являются необязательными параметрами значения, аt
представляет собой массив параметров.конечный пример
Объявление метода создает отдельное пространство объявления (§7.3) для параметров и параметров типа. Имена вводятся в это пространство объявления с помощью списка параметров типа и списка параметров метода. Текст метода, если таковой имеется, считается вложенным в это пространство объявления. В пространстве объявления метода является ошибкой наличие двух членов с одинаковыми именами.
Вызов метода (§12.8.10.2) создает копию, конкретную для этого вызова, параметры и локальные переменные метода, а список аргументов вызова присваивает значения или переменные ссылки на только что созданные параметры. В блоке метода параметры можно ссылаться по их идентификаторам в выражениях simple_name (§12.8.4).
Существуют следующие виды параметров:
- Параметры значения (§15.6.2.2).
- Входные параметры (§15.6.2.3.2).
- Выходные параметры (§15.6.2.3.4).
- Эталонные параметры (§15.6.2.3.3).
- Массивы параметров (§15.6.2.4).
Примечание. Как описано в §7.6,
in
out
модификаторы иref
модификаторы являются частью сигнатуры метода, ноparams
модификатор не является. конечная заметка
Параметры значений 15.6.2.2
Параметр, объявленный без модификаторов, является параметром значения. Параметр значения — это локальная переменная, которая получает исходное значение из соответствующего аргумента, предоставленного в вызове метода.
Правила определенного назначения см. в разделе "9.2.5".
Соответствующий аргумент в вызове метода должен быть выражением, которое неявно преобразуется (§10.2) в тип параметра.
Метод может назначать новые значения параметру значения. Такие назначения влияют только на расположение локального хранилища, представленное параметром значения. Они не влияют на фактический аргумент, заданный в вызове метода.
Параметры, передаваемые по ссылке 15.6.2.3
15.6.2.3.1 Общие положения
Входные, выходные и ссылочные параметры являются параметрами по ссылке. Параметр путем ссылки — это локальная ссылочная переменная (§9.7). Исходный референт получается из соответствующего аргумента, предоставленного в вызове метода.
Примечание. Значение параметра по ссылке можно изменить с помощью оператора присваивания ref (
= ref
).
Если параметр является параметром по ссылке, соответствующий аргумент в вызове метода должен состоять из соответствующего ключевого слова, in
, ref
, или out
, за которым следует variable_reference (§9.5) того же типа, что и параметр. Однако, если параметр является параметром in
, аргумент может быть выражением , для которого существует неявное преобразование (§10.2) из этого выражения аргумента в тип соответствующего параметра.
Ссылочные параметры не допускаются для функций, объявленных как итератор (§15.15) или асинхронной функции (§15.14).
В методе, который принимает несколько параметров по ссылке, возможно, что несколько имен могут представлять одно и то же место хранения.
Входные параметры 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); } }
Вызов
F
вG
передает ссылкуs
, которая относится как кa
, так и кb
. Таким образом, для этого вызова именаs
,a
иb
все относятся к одному и тому же месту хранения, а все три назначения изменяют поле экземпляраs
.конечный пример
Для типа struct
, в методе экземпляра, методе доступа экземпляра (§12.2.1) или конструкторе экземпляра с инициализатором конструктора, ключевое слово this
ведет себя точно так же, как параметр ссылки типа структуры (§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
Обратите внимание, что переменные
dir
иname
могут быть неназначенными до их передачи в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
на экземпляре с типом времени компиляции C
и типом времени выполнения R
(где R
является либо классом C
, производным от C
), вызов обрабатывается следующим образом:
- При привязке разрешение перегрузки применяется к
C
,N
иA
, чтобы выбрать конкретный методM
из набора методов, объявленных и унаследованныхC
. Это описано в разделе "12.8.10.2". - Затем во время выполнения:
- Если
M
это не виртуальный метод,M
вызывается. - В противном случае
M
является виртуальным методом, и вызывается наиболее производная реализацияM
в отношенииR
.
- Если
Для каждого виртуального метода, объявленного в классе или унаследованного, существует наиболее производная реализация метода в отношении этого класса. Наиболее производная реализация виртуального метода M
относительно класса R
определяется следующим образом:
- Если
R
содержит вводное виртуальное объявлениеM
, то это наиболее производная реализацияM
в отношенииR
. - В противном случае, если
R
содержит переопределениеM
, то это наиболее производная реализацияM
в отношенииR
. - В противном случае наиболее производная реализация в отношении
M
является той же, что и наиболее производная реализацияR
M
в отношении прямого базового класса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
, иD
невозможно переопределить метод, представленныйA
. Пример создает выходные данные:B.F B.F D.F D.F
Обратите внимание, что можно вызвать скрытый виртуальный метод путем доступа к экземпляру
D
через менее производный тип, в котором метод не скрыт.конечный пример
15.6.5 Переопределение методов
Если объявление метода экземпляра включает override
модификатор, метод считается переопределяющим методом. Метод переопределения переопределяет унаследованный виртуальный метод с той же сигнатурой. Хотя объявление виртуального метода вводит новый метод, объявление переопределения метода уточняет существующий унаследованный виртуальный метод, предоставляя новую реализацию этого метода.
Переопределяемый метод, за которым закреплено объявление переопределения, называется переопределенный базовый метод. Для метода переопределения, объявленного в классе M
, переопределенный базовый метод определяется путем анализа каждого базового класса C
, начиная с непосредственного базового класса C
и продолжая с каждым последующим прямым базовым классом, пока в заданном базовом классе не будет найден хотя бы один доступный метод с той же сигнатурой, что у C
, после подстановки аргументов типов. В целях обнаружения переопределенного базового метода метод считается доступным, если он public
, если он protected
, если он protected internal
или если он либо internal
либо private protected
, и объявлен в той же программе, что и C
.
Ошибка времени компиляции возникает, если не выполняются все указанные ниже условия для объявления переопределения.
- Переопределенный базовый метод можно найти, как описано выше.
- Существует именно один такой переопределенный базовый метод. Это ограничение действует, только если тип базового класса является созданным типом, где подстановка аргументов типа делает подпись двух методов одинаковой.
- Переопределенный базовый метод — это виртуальный, абстрактный или переопределенный метод. Другими словами, переопределенный базовый метод не может быть статическим или не виртуальным.
- Переопределенный базовый метод не является закрытым методом.
- Существует идентичное преобразование между возвращаемым типом переопределенного базового метода и типом возвращаемым методом переопределения.
- В объявлении переопределения и переопределенном базовом методе указана одна и та же доступность. Другими словами, операция переопределения не может изменить уровень доступа виртуального метода. Тем не менее, если переопределенный базовый метод объявлен как защищенный внутренний и находится в другой сборке, чем сборка, содержащая объявление переопределения, тогда объявленная доступность переопределения должна быть защищена.
- Объявление переопределения не содержит каких-либо type_parameter_constraints_clause. Вместо этого ограничения передаются от базового метода, который был переопределён. Ограничения, которые являются параметрами типа в переопределенном методе, могут быть заменены аргументами типа в наследуемом ограничении. Это может привести к ограничениям, которые недопустимы при явном указании, таких как типы значений или запечатанные типы.
Пример. Ниже показано, как работают переопределяющие правила для универсальных классов:
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()
инициирует вызов метода PrintFields, объявленного вB
. A base_access отключает механизм виртуального вызова и просто рассматривает базовый метод как обычный метод. Если бы вызов вB
был записан как((A)this).PrintFields()
, он рекурсивно вызывал бы методPrintFields
, объявленный вB
, а не тот, который объявлен в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
, и предупреждение сообщается, поскольку объявление не включает новый модификатор.конечный пример
Пример. В следующем коде
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
, и их параметры не должны быть выходными параметрами. Идентификатор partial
распознается как контекстное ключевое слово (§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.5).Ошибка компиляции возникает при попытке сослаться на
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.14.1). В противном случае эффективный тип возвращаемого значения для метода, не являющегося асинхронным, соответствует его типу возвращаемого значения, а эффективный тип возвращаемого значения для асинхронного метода с типом «TaskType»<T>
(§15.14.1) соответствует T
.
Если фактический возвращаемый тип метода void
и метод с блоковым телом, инструкции return
(§13.10.5) в блоке не должны указывать выражение. Если выполнение блока метода void завершается нормально (т. е. управление переходит за пределы тела метода), этот метод просто возвращается вызывающему коду.
Если эффективный тип возвращаемого метода void
и метод имеет тело выражения, выражение E
должно быть statement_expression, а тело точно эквивалентно блоку формы { E; }
.
Для метода return-by-value (§15.6.1) каждая инструкция return в тексте этого метода должна указывать выражение, которое неявно преобразуется в действующий тип возвращаемого значения.
Для метода возврата по ссылке (§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'
| 'readonly' // direct struct members only
| 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).
объявление свойства может включать набор атрибутов (§22) и любую из разрешенных видов объявленной доступности (§15.3.6), new
(§15.3.5), static
(§15.7.2), virtual
(§15.6.4, §15.7.6), override
(§15.6.5, §15.7.6), sealed
(§15.6.6), abstract
(§15.6.7, §15.7.6) и extern
(§15.6.8). Кроме того, property_declaration, которое содержится непосредственно в struct_declaration, может включать readonly
модификатор (§16.4.11).
- Первый объявляет свойство, имеющее тип значения, а не тип ссылки. Его значение имеет тип type. Это свойство может быть доступно для чтения и /или записи.
- Второй объявляет свойство типа ref. Его значением является variable_reference (§9.5), которое может быть
readonly
ссылкой на переменную типа type. Это свойство доступно только для чтения.
Property_declaration может включать набор атрибутов (
Объявления свойств применяются к тем же правилам, что и объявления методов (§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; } }
инструкции, и поэтому может быть использовано только для указания свойств только для чтения, где результат метода доступа get задается одним выражением.
Property_initializer может быть задано только для автоматически реализованного свойства (§15.7.4) и вызывает инициализацию базового поля таких свойств со значением, заданным выражением.
Ref_property_body может состоять из текста инструкции или текста выражения. В теле инструкции get_accessor_declaration объявляет get-аксессор (§15.7.3) свойства. Акцессор задает исполняемые инструкции, связанные с чтением свойства.
В ref_property_body текст выражения, состоящий из =>
, за которым следует ref
, variable_referenceV
и точка с запятой, точно эквивалентен телу инструкции { get { return ref V; } }
.
Примечание. Несмотря на то, что синтаксис для доступа к свойству аналогичен значению поля, свойство не классифицируется как переменная. Таким образом, невозможно передать свойство в качестве аргумента
in
,out
илиref
, если свойство имеет ссылочное значение и поэтому возвращает ссылку на переменную (§9.7). конечная заметка
Если объявление свойства включает 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'
| 'readonly' // direct struct members only
;
accessor_body
: block
| '=>' expression ';'
| ';'
;
ref_get_accessor_declaration
: attributes? accessor_modifier? 'get' ref_accessor_body
;
ref_accessor_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
Декларации аксессоров состоят из декларации получения аксессора, декларации установки аксессора или обоих. Каждое объявление аксессора состоит из необязательных атрибутов, необязательных accessor_modifier, токена get
или set
, за которыми следует accessor_body.
Для свойства ссылочного типа ref_get_accessor_declaration состоит из необязательных атрибутов, необязательного accessor_modifier, маркера get
, а затем ref_accessor_body.
Использование accessor_modifiers регулируется следующими ограничениями:
- accessor_modifier не может использоваться в интерфейсе или в явной реализации элемента интерфейса.
-
Accessor_modifier
readonly
допускается только в property_declaration или indexer_declaration, которая непосредственно содержится в struct_declaration (§16.4.11, §16.4.13). - Для свойства или индексатора, не имеющего модификатора
override
, модификатор accessor_modifier разрешён только в том случае, если свойство или индексатор имеет оба аксессора: get и set, и тогда он разрешён только для одного из этих аксессоров. - Для свойства или индексатора с модификатором
override
, метод доступа должен соответствовать модификатору accessor_modifier, если он есть, того метода доступа, который переопределяется. - Accessor_modifier должен объявлять доступ, который является более строгим, чем объявленный доступ самого свойства или индексатора. Чтобы быть точным:
- Если свойство или индексатор имеет объявленную доступность
public
, то доступность, объявленная accessor_modifier, может иметь значениеprivate protected
,protected internal
,internal
,protected
илиprivate
. - Если свойство или индексатор имеет объявленную доступность
protected internal
, то доступность, объявленная accessor_modifier, может иметь значениеprivate protected
,protected private
,internal
,protected
илиprivate
. - Если свойство или индексатор имеет объявленную доступность
internal
илиprotected
, то доступность, объявленная с помощью accessor_modifier, должна бытьprivate protected
илиprivate
. - Если у свойства или индексатора объявлена доступность
private protected
, то доступность, указанная модификатором accessor_modifier, должна бытьprivate
. - Если свойство или индексатор имеет объявленную доступность
private
, модификатор доступа не может использоваться.
- Если свойство или индексатор имеет объявленную доступность
Для свойств abstract
и extern
, не содержащих ссылку, любой accessor_body для каждого указанного аксессора — это просто точка с запятой. Неабстрактное, невнешнее свойство, но не индексатор, также может иметь accessor_body в виде точки с запятой для всех аксессоров, в этом случае это автоматически реализованное свойство (§15.7.4). Автоматически реализованное свойство должно иметь по крайней мере аксессор get. Для аксессоров любого другого неабстрактного, невнешнего свойства accessor_body является либо:
- блок, определяющий инструкции, выполняемые при вызове соответствующего аксессора; или
- Тело выражения, которое состоит из
=>
, за которым следует выражение и точка с запятой, и обозначает одно выражение, которое выполняется при вызове соответствующего аксессора.
Для свойств со значением-ссылкой abstract
и extern
ref_accessor_body — это просто точка с запятой. Для аксессора любого другого неабстрактного, невнешнего свойства тело ref_accessor_body может быть:
- блок, указывающий операторы, выполняемые при вызове аксессора чтения, или
- тело выражения, которое состоит из
=>
, за которым следуетref
, переменная_ссылка и точка с запятой. Ссылка на переменную вычисляется при вызове аксессора get.
Метод доступа get для свойства, не имеющего значения типа ссылочного, соответствует методу без параметров, возвращающему значение типа свойства. За исключением случаев, когда свойство используется как цель присваивания, если такое свойство упоминается в выражении, вызывается его аксессор для вычисления значения свойства (§12.2.2).
Тело метода доступа для свойства, значение которого не является ссылочным, должно соответствовать правилам методов, возвращающих значение, описанным в разделе 15.6.11. В частности, все return
инструкции в теле метода доступа get должны содержать выражение, которое неявно преобразуется в тип свойства. Кроме того, конечная точка метода доступа get не должна быть доступна.
Метод доступа get для ссылочного свойства соответствует методу без параметров с возвращаемым значением variable_reference к переменной типа свойства. Когда такое свойство используется в выражении, его аксессор вызывается для вычисления значения variable_reference этого свойства. Эта ссылка на переменную, как и любая другая, затем используется для чтения или, в случае переменных variable_reference, которые не являются только для чтения, для записи в указанную переменную в соответствии с контекстом.
Пример. В следующем примере показано свойство 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 должно соответствовать правилам для методов с типом возвращаемого значения ref, описанных в разделе 15.6.11.
Аксессор set соответствует методу с одним параметром значения типа свойства и типом возвращаемого значения void
. Неявный параметр модификатора всегда называется value
. Когда свойство обозначается как целевой объект присваивания (§12.21), или как операнд ++
или –-
(§12.8.16, §12.9.6), метод доступа set вызывается с аргументом, предоставляющим новое значение (§12.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
поле. Метод доступа набора проверяет, отличается ли новое значение от текущего значения, а если да, он сохраняет новое значение и перерисовывает элемент управления. Свойства часто следуют приведенному выше шаблону: аксессор get просто возвращает значение, хранящееся в полеprivate
, а аксессор set изменяет это полеprivate
и затем выполняет любые дополнительные действия, необходимые для полного обновления состояния объекта. Учитывая приведенныйButton
выше класс, ниже приведен пример использованияCaption
свойства:Button okButton = new Button(); okButton.Caption = "OK"; // Invokes set accessor string s = okButton.Caption; // Invokes get accessor
Здесь метод доступа set вызывается путем назначения значения свойству, а метод доступа get вызывается путем ссылки на свойство в выражении.
конечный пример
Методы доступа к свойству 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
вB
скрывает свойствоP
в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
. Обратите внимание, что приведение можно использовать для получения доступа к скрытому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
зависит от количества раз, когда к свойству ранее был доступ. Таким образом, доступ к свойству создает наблюдаемый побочный эффект, а свойство должно быть реализовано как метод.Соглашение "без побочных эффектов" для акцессоров get не означает, что акцессоры get всегда должны быть написаны просто для возвращения значений, которые уже хранятся в полях. Действительно, методы доступа часто вычисляют значение свойства, получая доступ к нескольким полям или вызывая методы. Однако правильно разработанный аксессор не выполняет никаких действий, которые вызывают наблюдаемые изменения в состоянии объекта.
конечный пример
Свойства можно использовать для задержки инициализации ресурса до момента его первой ссылки.
Пример:
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
содержит три свойства,In
Out
иError
, которые представляют стандартные входные, выходные и ошибки устройства соответственно. Предоставляя эти элементы в виде свойств,Console
класс может отложить их инициализацию до тех пор, пока они не будут использованы. Например, при первой ссылке на свойствоOut
, как вConsole.Out.WriteLine("hello, world");
Создается базовый
TextWriter
для выходного устройства. Однако если приложение не ссылается наIn
свойства иError
свойства, то для этих устройств не создаются объекты.конечный пример
15.7.4 Автоматически реализованные свойства
Автоматически реализованное свойство (или сокращенно авто-свойство) — это не абстрактное, не внешнее, не имеющее значения ref свойство с телом аксессора в виде только точек с запятой accessor_body. Автоматические свойства должны иметь аксессор get и могут при необходимости иметь аксессор set.
Если свойство указывается как автоматически реализованное свойство, скрытое поле резервного копирования автоматически доступно для свойства, а методы доступа реализуются для чтения и записи в это резервное поле. Скрытое резервное поле недоступно, оно может быть прочитано и записано только с помощью автоматически реализованных методов доступа к свойствам, даже в пределах содержащего типа. Если свойство 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, или индексатора с типом значения, отличным от ref, домены доступности конкретных аксессоров используются для определения допустимости.
- Если использование — это значение (§12.2.2), аксессор get должен существовать и быть доступным.
- Если использование является целевым объектом простого присваивания (§12.21.2), аксессор установки должен существовать и быть доступным.
- Если использование предназначено в качестве цели составного оператора присваивания (§12.21.4), или в качестве цели операторов
++
или--
(§12.8.16, §12.9.6), должны существовать и быть доступны как аксессоры получения, так и аксессоры установки.
Пример: В следующем примере свойство
A.Text
скрыто свойствомB.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 или индексатора со значением ref — используется ли оно как значение, как целевой объект простого присваивания или как целевой объект составного присваивания — область доступности используемого метода доступа используется для определения допустимости этого использования.
Аксессор, используемый для реализации интерфейса, не должен иметь 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
применяется ко всем не закрытым методам доступа свойства. Если метод доступа виртуального свойства имеет private
accessor_modifier, частный метод доступа неявно не является виртуальным.
Объявление абстрактного свойства указывает, что методы доступа свойства являются виртуальными, но не предоставляют фактическую реализацию методов доступа. Вместо этого неабстрактные производные классы должны предоставить собственную реализацию методов доступа путем переопределения свойства. Поскольку аксессор для объявления абстрактного свойства не предоставляет фактической реализации, его accessor_body просто состоит из точки с запятой. Абстрактное свойство не должно иметь private
акцессор.
Объявление свойства, включающее abstract
override
и модификаторы, указывает, что свойство абстрактно и переопределяет базовое свойство. Методы доступа такого свойства также абстрактны.
Объявления абстрактных свойств разрешены только в абстрактных классах (§15.2.2.2). Аксессоры унаследованного виртуального свойства можно переопределить в производном классе с помощью объявления свойства, которое указывает директиву override
. Это называется объявлением переопределяющего свойства. Объявление переопределяющего свойства не объявляет новое свойство. Вместо этого он просто специализируется на реализации методов доступа существующего виртуального свойства.
Объявление переопределения и замещённое базовое свойство должны иметь один и тот же заданный уровень доступности. Другими словами, декларация переопределения не должна изменять доступность базового свойства. Тем не менее, если переопределенное базовое свойство имеет уровень доступа 'protected internal' и объявляется в другой сборке, отличной от той, которая содержит объявление переопределения, то объявление переопределения должно иметь уровень доступа 'protected'. Если унаследованное свойство имеет только один метод доступа (т. е. если унаследованное свойство доступно только для чтения или только для записи), то переопределяющее свойство должно включать только этот метод доступа. Если унаследованное свойство включает оба метода доступа (т. е. если унаследованное свойство является чтением и записью), то переопределяющее свойство может включать один метод доступа или оба метода доступа. Должно быть идентичное преобразование между типом переопределения и унаследованным свойством.
Объявление переопределяющего свойства может включать модификатор sealed
. Использование этого модификатора предотвращает дальнейшее переопределение свойства производным классом. Методы доступа к запечатанному свойству также запечатаны.
За исключением различий в синтаксисе объявления и вызова, виртуальные, запечатанные, переопределяющие и абстрактные аксессоры ведут себя точно так же, как виртуальные, запечатанные, переопределяющие и абстрактные методы. В частности, правила, описанные в §15.6.4, §15.6.5, §15.6.6 и §15.6.7 применяются, как если бы методы доступа были методами соответствующей формы:
- Метод доступа get соответствует методу без параметров с возвращаемым значением типа свойства и с такими же модификаторами, что и у содержащего свойства.
- Установщик соответствует методу с одним параметром значения типа свойства, типом возвращаемого значения void и теми же модификаторами, что и содержащего свойства.
Пример. В следующем коде
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; } }
Здесь объявления
X
,Y
иZ
переопределяют объявления свойств. Каждое объявление свойства точно соответствует модификаторам специальных возможностей, типу и имени соответствующего унаследованного свойства. Аксессор чтенияX
и аксессор записиY
используют ключевое слово base для доступа к унаследованным аксессорам. Объявление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'
| 'readonly' // direct struct members only
| 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.3.5), static
(§15.6.3, §15.8.4), virtual
(§15.6.4, §15.8.5), override
(§15.6.5, §15.8.5), sealed
(§15.6.6), abstract
(§15.6.7, §15.8.5) и extern
(§15.6.8) модификаторов. Кроме того, event_declaration, который содержится непосредственно в struct_declaration, может включать readonly
модификатор (§16.4.12).
Объявления событий применяются к тем же правилам, что и объявления методов (§15.6) в отношении допустимых сочетаний модификаторов.
Тип объявления события должен быть delegate_type (§8.2.8), и что delegate_type должно быть по крайней мере так же доступно, как само событие (§7.5.5).
Объявление события может включать event_accessor_declarations. Однако если это не так, для неисстрактных событий компилятор должен предоставлять их автоматически (§15.8.2); для событий extern
методы доступа предоставляются внешним образом.
Объявление события, которое опускает определение аксессоров события, задает одно или несколько событий — по одному на каждый декларатор переменной. Атрибуты и модификаторы применяются ко всем элементам, объявленным в таком event_declaration.
Это ошибка времени компиляции, если event_declaration включает как модификатор abstract
, так и event_accessor_declaration.
Если объявление события включает extern
модификатор, событие считается внешним событием. Поскольку объявление внешнего события не предоставляет фактической реализации, является ошибкой включать модификатор extern
и event_accessor_declarations.
Это ошибка компиляции, если в объявлении события с модификатором или модификатором abstract
у external
указан variable_initializer.
Событие можно использовать в качестве левого операнда операторов +=
и -=
. Эти операторы используются соответственно для присоединения обработчиков событий к событию или удаления их, а модификаторы доступа события определяют контексты, в которых такие операции разрешены.
Единственные операции, разрешенные для события кодом, который находится вне типа, в котором объявлено это событие, — это +=
и -=
. Таким образом, хотя такой код может добавлять и удалять обработчики для события, он не может напрямую получить или изменить базовый список обработчиков событий.
В операции формы x += y
или x –= y
, когда x
является событием, результат операции имеет тип void
(§12.21.5) (в отличие от типа , со значением x
после присваивания, как для других операторов x
и +=
, определённых на не-событийных типах). Это препятствует внешнему коду косвенно получать доступ к базовому делегату события.
Пример. В следующем примере показано, как обработчики событий присоединены к экземплярам
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
событие. Концепция создания события в точности соответствует вызову делегата, представленного этим событием. Это позволяет обойтись без особой языковой конструкции для создания событий. Обратите внимание, что перед вызовом делегата выполняется проверка, которая гарантирует, что делегат не является пустым и что проверка выполняется на локальной копии, чтобы обеспечить безопасность потока.Вне объявления класса
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 */ } } }
Внутри класса
X
, ссылки наEv
в левой части операторов+=
и–=
инициируют вызов методов добавления и удаления. Все прочие ссылки наEv
компилируются как ссылки на скрытое поле__Ev
(§12.8.7). Имя "__Ev
" является произвольным; скрытое поле может иметь любое имя или ни одно имя вообще.конечная заметка
15.8.3 Методы доступа к событиям
Примечание: Объявления событий обычно опускают объявления модификаторов доступа события, как показано в приведённом выше
Button
примере. Например, они могут быть включены, если неприемлема стоимость хранения одного поля для каждого события. В таких случаях класс может включать event_accessor_declarationи использовать частный механизм для хранения списка обработчиков событий. конечная заметка
event_accessor_declarations события задают исполняемые инструкции, связанные с добавлением и удалением обработчиков событий.
Объявления доступа состоят из add_accessor_declaration и remove_accessor_declaration. Каждое объявление аксессора состоит из токена add или remove, за которыми следует блок. Блок, связанный с 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_declaration.
Объявление события, включающее abstract
override
и модификаторы, указывает, что событие абстрактно и переопределяет базовое событие. Аксессоры такого события также абстрактны.
Объявления абстрактных событий разрешены только в абстрактных классах (§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'
| 'readonly' // direct struct members only
| 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 может включать набор атрибутов (§22) и любой из разрешенных видов объявленной доступности (§15.3.6), new
(§15.3.5), virtual
(§15.6.4), override
(§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7) и extern
(§15.6.8) модификаторов. Кроме того, indexer_declaration, который содержится непосредственно в struct_declaration, может включать readonly
модификатор (§16.4.12).
- Первый объявляет индексатор, не использующий ссылки. Его значение имеет тип type. Этот тип индексатора может быть доступен для чтения и /или записи.
- Второй объявляет индексатор со значением типа ref. Его значением является variable_reference (§9.5), которое может быть
readonly
ссылкой на переменную типа type. Этот тип индексатора доступен только для чтения.
Indexer_declaration может включать набор атрибутов (§22) и любой из разрешённых видов объявленной доступности (§15.3.6), new
(§15.3.5), virtual
(§15.6.4), override
(§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7), и extern
(§15.6.8) модификаторов.
Объявления индексатора применяются к тем же правилам, что и объявления методов (§15.6) в отношении допустимых сочетаний модификаторов, при этом одно исключение заключается в том, что static
модификатор не допускается в объявлении индексатора.
Тип объявления индексатора указывает тип элемента индексатора, представленного объявлением.
Примечание: так как индексаторы предназначены для использования в контекстах, подобных элементам массива, термин тип элемента, определенный для массива, также используется с индексатором. конечная заметка
Если индексатор не является явной реализацией элемента интерфейса, за типом следует ключевое слово this
. Для явной реализации члена интерфейса за типом следует (interface_type), ".
" и ключевое слово this
. В отличие от других членов, индексаторы не имеют определяемых пользователем имен.
Parameter_list задает параметры индексатора. Список параметров индексатора соответствует списку параметров метода (§15.6.2), за исключением того, что должен быть указан хотя бы один параметр, и что модификаторы параметров this
, ref
и out
запрещены.
Тип индексатора и каждого из типов, на которые ссылается parameter_list, должен быть по крайней мере так же доступен, как сам индексатор (§7.5.5).
Индексатор может состоять из тела инструкции (§15.7.1) или тела выражения (§15.6.1). В теле инструкции accessor_declarations, которые должны быть заключены в маркеры "{
" и "}
", объявляются методы доступа (§15.7.3) индексатора. Методы доступа указывают исполняемые инструкции, связанные с чтением и записью элементов индексатора.
В indexer_body тело выражения, состоящее из "=>
", за которым следует выражение E
и точка с запятой, точно эквивалентно телу инструкции { get { return E; } }
, и поэтому может использоваться только для указания индексаторов, доступных только для чтения, где результат аксессора get определяется одним выражением.
Ref_indexer_body может состоять из текста инструкции или текста выражения. В теле инструкции get_accessor_declaration объявляет метод получения доступа (§15.7.3) индексатора. Аксессор определяет исполняемые операторы, связанные с чтением индексатора.
В выражении ref_indexer_body тело выражения, состоящее из =>
за которым следует ref
, ссылка на переменнуюV
и точка с запятой, точно эквивалентно телу инструкции { get { return ref V; } }
.
Примечание. Несмотря на то, что синтаксис для доступа к элементу индексатора совпадает с тем, что для элемента массива, элемент индексатора не классифицируется как переменная. Таким образом, невозможно передать элемент индексатора в качестве
in
,out
, илиref
аргумента, если только индексатор не имеет ссылочного значения и, следовательно, возвращает ссылку (§9.7). конечная заметка
Параметры индексатора определяют сигнатуру (§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.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) и постфиксные операторы допуска нуля (§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
описаны далее в §12.24.
Пример. В следующем примере показана реализация и последующее использование оператора++ для целочисленного векторного класса:
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
илиT?
, а второй из которых должен иметь типint
илиint?
, и может возвращать любой тип.
Подпись двоичного оператора состоит из маркера оператора (+
, -
*
/
%
&
|
^
<<
>>
==
!=
>
<
>=
или <=
) и типов двух параметров. Возвращаемый тип и имена параметров не являются частью подписи двоичного оператора.
Для некоторых двоичных операторов требуется попарное объявление. Для каждого объявления любого оператора пары должно быть соответствующее объявление другого оператора пары. Два объявления оператора совпадают, если существуют преобразования идентичности между их возвращаемыми типами и соответствующими типами параметров. Для следующих операторов необходимо попарное объявление:
- оператор
==
и оператор!=
- оператор
>
и оператор<
- оператор
>=
и оператор<=
Операторы преобразования 15.10.4
Объявление оператора преобразования представляет определяемое пользователем преобразование (§10.5), которое расширяет предварительно определенные неявные и явные преобразования.
Объявление оператора преобразования, включающее implicit
ключевое слово, представляет неявное преобразование, определяемое пользователем. Неявные преобразования могут выполняться в различных ситуациях, включая вызовы элементов функции, приведение типов и назначение. Это описано далее в разделе "10.2".
Объявление оператора преобразования, включающее explicit
ключевое слово, представляет явное преобразование, определяемое пользователем. Явные преобразования могут иметь место в выражениях приведения и подробно описаны в разделе 10.3.
Оператор преобразования преобразуется из исходного типа, указанного типом параметра оператора преобразования, в целевой тип, указанный возвращаемым типом оператора преобразования.
Для заданного исходного типа S
и целевого типа T
, если S
или T
являются типами значений, допускающими значение NULL, пусть S₀
и T₀
ссылаются на их базовые типы; в противном случае S₀
и T₀
равны S
и T
соответственно. Класс или структуру разрешено объявлять преобразование из исходного типа S
в целевой тип T
, только если все из следующих значений имеют значение true:
S₀
иT₀
являются разными типами.Либо
S₀
, либоT₀
является типом экземпляра класса или структуры, содержащей объявление оператора.Ни
S₀
ниT₀
не являются interface_type.За исключением определяемых пользователем преобразований, преобразование не существует из
S
вT
или изT
вS
.
В целях этих правил любые параметры типа, связанные с S
или T
, считаются уникальными типами, которые не имеют отношений наследования с другими типами, и любые ограничения на эти параметры типа игнорируются.
Пример. В следующем примере:
class C<T> {...} class D<T> : C<T> { public static implicit operator C<int>(D<T> value) {...} // Ok public static implicit operator C<string>(D<T> value) {...} // Ok public static implicit operator C<T>(D<T> value) {...} // Error }
Первые два объявления операторов разрешены, так как
T
иint
, иstring
соответственно считаются уникальными типами, не имеющими связи. Однако третий оператор является ошибкой, так какC<T>
является базовым классомD<T>
.конечный пример
Из второго правила следует, что оператор преобразования должен преобразовать либо в класс или тип структуры, либо из них, в которых объявлен оператор.
Пример: для класса или типа структуры
C
можно определить преобразование изC
вint
и изint
вC
, но не изint
вbool
. конечный пример
Невозможно напрямую переопределить предварительно определенное преобразование. Таким образом, операторам преобразования не разрешается преобразовывать из или в object
, потому что между object
и всеми другими типами уже существуют неявные и явные преобразования. Аналогичным образом, ни исходные, ни целевые типы преобразования не могут быть базовым типом другого, так как преобразование уже существует.
Однако можно объявить операторы универсальных типов, которые для определенных аргументов типов указывают преобразования, которые уже существуют в качестве предварительно определенных преобразований.
Пример:
struct Convertible<T> { public static implicit operator Convertible<T>(T value) {...} public static explicit operator T(Convertible<T> value) {...} }
если тип указан в качестве аргумента типа
object
дляT
, второй оператор объявляет преобразование, которое уже существует (неявное, а следовательно, явное преобразование существует из любого типа в объект типа).конечный пример
В случаях, когда предварительно определенное преобразование существует между двумя типами, любые пользовательские преобразования между этими типами игнорируются. В частности:
- Если предварительно определенное неявное преобразование (§10.2) существует из типа
S
в типT
, все пользовательские преобразования (неявные или явные) изS
вT
игнорируются. - Если предварительно определенное явное преобразование (§10.3) существует от типа
S
к типуT
, все определяемые пользователем явные преобразования изS
вT
игнорируются. Кроме того:- Если
S
илиT
является типом интерфейса, пользовательские неявные преобразования изS
вT
игнорируются. - В противном случае определяемые пользователем неявные преобразования из
S
вT
по-прежнему рассматриваются.
- Если
Для всех типов, кроме object
, операторы, объявленные типом Convertible<T>
выше, не конфликтуют с предварительно определенными преобразованиями.
Пример:
void F(int i, Convertible<int> n) { i = n; // Error i = (int)n; // User-defined explicit conversion n = i; // User-defined implicit conversion n = (Convertible<int>)i; // User-defined implicit conversion }
Однако для типа
object
предварительно определенные преобразования скрывают определяемые пользователем преобразования во всех случаях, кроме одного.void F(object o, Convertible<object> n) { o = n; // Pre-defined boxing conversion o = (object)n; // Pre-defined boxing conversion n = o; // User-defined implicit conversion n = (Convertible<object>)o; // Pre-defined unboxing conversion }
конечный пример
Определяемые пользователем преобразования не допускаются для конвертации из 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); }
Преобразование из
Digit
вbyte
неявное, так как оно никогда не создает исключения или не теряет информации, но преобразование изbyte
вDigit
является явным, так как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).
Объявление конструктора может включать набор атрибутов (§22), любой из разрешённых видов объявленной доступности (§15.3.6), и модификатор extern
(§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 состоит либо из
- блок, указывающий выражения для инициализации нового экземпляра класса; или
- тело выражения, состоящее из
=>
, за которым следует выражение и точка с запятой, и обозначающее одно выражение для инициализации нового экземпляра класса.
Конструктор_body, который является блоком или телом выражения, точно соответствует блоку метода экземпляра с void
типом возвращаемого значения (§15.6.11).
Конструкторы объектов не наследуются. Таким образом, класс не имеет конструкторов экземпляров, отличных от фактически объявленных в классе, за исключением того, что если класс не содержит объявлений конструктора экземпляров, конструктор экземпляра по умолчанию автоматически предоставляется (§15.11.5).
Конструкторы экземпляров вызываются выражениями создания объектов object_creation_expression (§12.8.17.2) и инициализаторами конструктора constructor_initializer.
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) {} }
конечный пример
Инициализатор конструктора экземпляра не может получить доступ к созданному экземпляру. Поэтому предпочтительно не обращаться к this в выражении аргумента инициализатора конструктора, так как это ошибка компиляции, если выражение ссылки на член экземпляра использует 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() {}
или
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).
Объявление статического конструктора может включать набор атрибутов (§22) и extern
(§15.6.8).
Идентификатор static_constructor_declaration должен называть класс, в котором объявлен статический конструктор. Если указано другое имя, возникает ошибка во время компиляции.
Если объявление статического конструктора включает extern
модификатор, статический конструктор считается внешним статическим конструктором. Поскольку объявление внешнего статического конструктора не предоставляет фактической реализации, его static_constructor_body состоит из точки с запятой. Для всех других объявлений статических конструкторов тело static_constructor_body может состоять из одного из следующих компонентов.
- блок, указывающий инструкции для выполнения для инициализации класса; или
- Тело выражения, состоящее из
=>
, за которым следует выражение и точка с запятой, обозначает одно выражение для выполнения с целью инициализации класса.
Static_constructor_body
Статические конструкторы не наследуются и не могут вызываться напрямую.
Статический конструктор для закрытого класса выполняется не более одного раза в определенном домене приложения. Выполнение статического конструктора активируется первым из следующих событий, которые происходят в домене приложения:
- Создается экземпляр класса.
- Выполняется ссылка на любые статические члены класса.
Если класс содержит 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
, в свою очередь, переходит к вычислению значенияX
, и при этом извлекает значение по умолчаниюY
, равное нулю.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).
Объявление финализатора может включать набор атрибутов(§22).
Идентификатор декларатора завершения должен называться именем класса, в котором объявлен метод завершения. Если указано другое имя, возникает ошибка во время компиляции.
Когда объявление завершителя включает extern
модификатор, завершитель считается внешним завершителем. Поскольку объявление внешнего финализатора не предоставляет фактической реализации, его finalizer_body состоит из точки с запятой. Для всех остальных финализаторов finalizer_body состоит либо из
- блок, указывающий инструкции, выполняемые для завершения экземпляра класса.
- или тела выражения, которое состоит из
=>
, за которым следует выражение и точка с запятой, и обозначает одно выражение для выполнения с целью завершения экземпляра класса.
Finalizer_body
Финализаторы не наследуются. Таким образом, класс не имеет финализаторов, отличных от того, который может быть объявлен в этом классе.
Примечание. Так как метод завершения не требует параметров, его нельзя перегружать, поэтому класс может иметь, по крайней мере, один метод завершения. конечная заметка
Финализаторы вызываются автоматически и не могут вызываться явно. Экземпляр становится подлежащим завершению, когда больше ни один код не может использовать этот экземпляр. Выполнение финализатора для экземпляра может произойти в любое время после того, как экземпляр станет подлежащим завершению (§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
так как финализаторы в цепочке наследования вызываются в порядке, от наиболее наследуемых до наименее наследуемых.
конечный пример
Методы завершения реализуются путем переопределения виртуального метода Finalize
на System.Object
. Программам на C# не разрешается переопределять этот метод или вызывать его (или его переопределения) непосредственно.
Пример: например, программа
class A { override protected void Finalize() {} // Error public void F() { this.Finalize(); // Error } }
содержит две ошибки.
конечный пример
Компилятор должен вести себя так, как если бы этот метод и его переопределения вообще не существовали.
Пример. Таким образом, эта программа:
class A { void Finalize() {} // Permitted }
является допустимым, и метод, показанный, скрывает метод
System.Object
Finalize
.конечный пример
Для обсуждения поведения при возникновении исключения из финализатора см. в разделе §21.4.
15.14 Асинхронные функции
15.14.1 Общие
Метод (§15.6) или анонимная функция (§12.19) с async
модификатором называется асинхронная функция. Термин асинхронный используется для описания любой функции, которая имеет async
модификатор.
Во время компиляции является ошибкой, если в списке параметров асинхронной функции указаны параметры in
, out
, ref
или параметры любого типа ref struct
.
Return_type асинхронного метода должен быть либо void
типом задачи, либо типом асинхронного итератора (§15.15). Для асинхронного метода, создающего значение результата, тип задачи или асинхронный итератор (§15.15.3) должен быть универсальным. Для асинхронного метода, который не создает результирующего значения, тип Task не должен быть обобщенным. Такие типы называются в этой спецификации «TaskType»<T>
и «TaskType»
соответственно. Тип System.Threading.Tasks.Task
и типы стандартной библиотеки, созданные из System.Threading.Tasks.Task<TResult>
и System.Threading.Tasks.ValueTask<T>
являются типами задач, а также классом, структурой или типом интерфейса, связанным с типом построителя задач с помощью атрибута 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.14.2). Тип построителя задач должен точно соответствовать объявленной доступности соответствующего типа задачи.
Примечание: Если тип задачи объявлен
internal
, то соответствующий тип построителя также должен быть объявленinternal
и определен в той же сборке. Если тип задачи вложен внутри другого типа, то тип задачи builder также должен быть вложен в тот же тип. конечная заметка
Асинхронная функция имеет возможность приостановить оценку с помощью выражений await (§12.9.8) в тексте. Позже оценка может быть возобновлена в точке приостановки выражения ожидания с помощью делегата возобновления. Делегат возобновления имеет тип System.Action
, и при его вызове обработка вызова асинхронной функции резюмируется от выражения await, на котором она была прервана.
Текущий вызывающий объект асинхронной функции — это исходный вызывающий объект, если вызов функции никогда не был приостановлен, или самый последний вызывающий делегат возобновления в противном случае.
Шаблон построителя типов задач 15.14.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)
может быть вызвано сгенерированной компилятором реализацией, чтобы определить экземпляр построителя, связанного с экземпляром машины состояний, особенно когда машина состояний реализуется как тип значения.- Если построитель вызывает
, то вызовет для экземпляра построителя, связанного с .
- Если построитель вызывает
Примечание: Для
SetResult(T result)
и«TaskType»<T> Task { get; }
параметр и аргумент соответственно должны быть идентично преобразуемыми вT
. Это позволяет конструктору типов поддерживать такие типы, как кортежи, где два типа, которые не одинаковы, могут быть преобразованы, сохраняя идентичность. конечная заметка
15.14.3. Оценка асинхронной функции, возвращающей задачу
Вызов асинхронной функции, возвращающей задачу, приводит к созданию экземпляра типа возвращаемой задачи. Это называется задачей возврата асинхронной функции. Задача изначально находится в неполном состоянии.
Затем тело асинхронной функции вычисляется до тех пор, пока не будет приостановлено (достигнув выражения await) или завершится, после чего управление возвращается вызывающему вместе с задачей возврата.
Когда тело асинхронной функции завершается, задача возврата выводится из неполного состояния.
- Если тело функции завершается в результате достижения инструкции возврата или конца тела, любое значение результата записывается в задачу возврата, которая помещается в состояние успешного выполнения.
- Если тело функции завершается из-за необработанного
OperationCanceledException
, исключение записывается в задачу возврата, которая помещается в отмененное состояние. - Если тело функции завершается в результате любого другого неухваченного исключения (§13.10.6), исключение записывается в возвращаемой задаче, которая помещается в ошибочное состояние.
15.14.4 Оценка асинхронной функции, возвращающей пустоту
Если возвращаемый тип асинхронной функции void
не возвращается, оценка отличается от вышеуказанного: поскольку задача не возвращается, функция вместо этого информирует о завершении и исключениях в контексте синхронизации текущего потока. Точное определение контекста синхронизации зависит от реализации, но представляет собой представление "где" текущий поток выполняется. Контекст синхронизации уведомляется, когда начинается выполнение асинхронной функции, возвращающей значение void
, успешно завершается или вызывает неперехваченное исключение.
Это позволяет контексту отслеживать, сколько асинхронных функций, возвращающих void
, выполняется под ним, и определять, как распространять возникающие из них исключения.
15.15 Синхронные и асинхронные итераторы
15.15.1 Общее
Элемент функции (§12.6) или локальная функция (§13.6.4), реализованная с помощью блока итератора (§13.3), называется итератором. Блок итератора может использоваться в качестве части функции, если возвращаемый тип соответствующей части функции является одним из интерфейсов перечислителя (§15.15.2) или одним из интерфейсов перечисляемых объектов (§15.15.3).
Асинхронная функция (§15.14), реализованная с помощью блока итератора (§13.3), называется асинхронным итератором. Блок асинхронного итератора может использоваться в качестве тела метода функции, если возвращаемый тип этого метода является интерфейсами асинхронного перечислителя (§15.15.2) или асинхронными интерфейсами перечисляемого (§15.15.3).
Блок итератора может возникать как method_body, operator_body или accessor_body, в то время как события, конструкторы экземпляров, статические конструкторы и метод завершения не должны быть реализованы как синхронные или асинхронные итераторы.
Если член функции или локальная функция реализуется с помощью блока итератора, это ошибка времени компиляции, если в списке параметров члена функции указаны параметры in
, out
, или ref
или параметр типа ref struct
.
15.15.2 Интерфейсы перечислителя
Интерфейсы перечислителя — это не универсальный интерфейс System.Collections.IEnumerator
и все экземпляры универсальных интерфейсовSystem.Collections.Generic.IEnumerator<T>
.
Интерфейсы асинхронного перечислителя — это все экземпляры универсального интерфейсаSystem.Collections.Generic.IAsyncEnumerator<T>
.
Для краткости, в этом подпункте и его аналогичных разделах эти интерфейсы упоминаются как IEnumerator
, IEnumerator<T>
, и IAsyncEnumerator<T>
соответственно.
Интерфейсы перечисления 15.15.3
Перечисляемые интерфейсы — это необобщённый интерфейс System.Collections.IEnumerable
и все экземпляры универсальных интерфейсовSystem.Collections.Generic.IEnumerable<T>
.
Асинхронные перечисляемые интерфейсы — это все экземпляры универсального интерфейсаSystem.Collections.Generic.IAsyncEnumerable<T>
.
Для краткости, в этом подпункте и его аналогичных разделах эти интерфейсы упоминаются как IEnumerable
, IEnumerable<T>
, и IAsyncEnumerable<T>
соответственно.
Тип доходности 15.15.4
Итератор создает последовательность значений всех одинаковых типов. Этот тип называется типом доходности итератора.
- Тип доходности итератора, который возвращает
IEnumerator
илиIEnumerable
являетсяobject
. - Тип возвращаемого значения итератора, который возвращает
IEnumerator<T>
,IAsyncEnumerator<T>
,IEnumerable<T>
илиIAsyncEnumerable<T>
, — этоT
.
Объекты перечислителя 15.15.5
15.15.5.1 Общие
Если член функции или локальная функция, возвращающая тип интерфейса перечислителя, реализуется с помощью блока итератора, вызов функции не приводит к немедленному выполнению кода в блоке итератора. Вместо этого создается и возвращается объект перечислителя. Этот объект инкапсулирует код, указанный в блоке итератора, и выполнение кода в блоке итератора происходит при вызове объекта MoveNext
перечислителя или MoveNextAsync
метода. Объект перечислителя имеет следующие характеристики:
- Он реализует
System.IDisposable
,IEnumerator
иIEnumerator<T>
, илиSystem.IAsyncDisposable
иIAsyncEnumerator<T>
, гдеT
является типом возвращаемого значения итератора. - Он инициализирован с копией значений аргументов (если таковые имеются) и значения экземпляра, переданные члену функции.
- Он имеет четыре потенциальных состояния, до, запуск, приостановку и после, и первоначально находится в состоянии до.
Объект перечислителя обычно является экземпляром класса перечислителя, созданного компилятором, который инкапсулирует код в блоке итератора и реализует интерфейсы перечислителя, но возможны другие методы реализации. Если класс перечислителя создается компилятором, этот класс будет вложен напрямую или косвенно в класс, содержащий член функции, он будет иметь частные специальные возможности, и он будет иметь имя, зарезервированное для использования компилятором (§6.4.3).
Объект перечислителя может реализовать больше интерфейсов, чем указанные выше.
В следующих подклаузах описывается необходимое поведение члена для продвижения перечислителя, получения текущего значения из перечислителя и освобождения ресурсов, используемых перечислителем. Они определены в следующих элементах для синхронных и асинхронных перечислителей соответственно:
- Для продвижения перечислителя:
MoveNext
иMoveNextAsync
. - Чтобы получить текущее значение:
Current
. - Избавиться от ресурсов:
Dispose
иDisposeAsync
.
Объекты перечислителя не поддерживают IEnumerator.Reset
метод. Вызов этого метода вызывает выброс System.NotSupportedException
.
Синхронные и асинхронные блоки итератора отличаются в том, что элементы асинхронного итератора возвращают типы задач и могут ожидаться.
Продвинуть перечислитель
Методы MoveNext
и MoveNextAsync
объекта перечислителя инкапсулируют код блока итератора. Вызов метода MoveNext
или MoveNextAsync
исполняет код в блоке итератора и соответствующим образом задает свойство Current
объекта перечислителя.
MoveNext
bool
возвращает значение, значение которого описано ниже.
MoveNextAsync
возвращает значение ValueTask<bool>
(§15.14.3). Результат задачи, возвращённой из MoveNextAsync
, имеет то же значение, что и результат из MoveNext
. В следующем описании действия, описанные для MoveNext
, применяются к MoveNextAsync
со следующим различием: в случае, если указано, что MoveNext
возвращает true
или false
, MoveNextAsync
переводит задачу в состояние завершено и устанавливает значение результата задачи в соответствующее значение true
или false
.
Точное действие, выполняемое MoveNext
или MoveNextAsync
зависит от состояния объекта перечислителя при вызове:
- Если состояние объекта перечислителя перед, вызовите
MoveNext
:- Изменяет состояние на работу.
- Инициализирует параметры, включая
this
, блока итератора до значений аргументов и значения экземпляра, сохраненных при инициализации объекта перечислителя. - Выполняет блок итератора от начала до прерывания выполнения (как описано ниже).
- Если состояние объекта перечислителя — выполнение, результат вызова
MoveNext
не указан. - Если состояние объекта перечислителя приостановлено, вызов moveNext:
- Изменяет состояние на работу.
- Восстанавливает значения всех локальных переменных и параметров (включая
this
) до значений, сохранённых в момент последней приостановки выполнения блока итератора.Примечание. Содержимое любых объектов, на которые ссылается эти переменные, может измениться с момента предыдущего вызова
MoveNext
. конечная заметка - Возобновляет выполнение блока итератора сразу после инструкции yield return, приостановившей выполнение, и продолжается до тех пор, пока выполнение не будет прервано (как описано ниже).
- Если объект перечислителя находится в состоянии после, вызов
MoveNext
возвращает значение false.
Когда MoveNext
выполняет блок итератора, выполнение может быть прервано четырьмя способами: оператором yield return
, оператором yield break
, при обнаружении конца блока итератора, а также исключением, выбрасываемым и распространяемым из блока итератора.
- При встрече с выражением
yield return
(§9.4.4.20):- Выражение, заданное в инструкции, вычисляется, неявно преобразуется в тип доходности и назначается
Current
свойству объекта перечислителя. - Выполнение текста итератора приостановлено. Значения всех локальных переменных и параметров (включая
this
) сохраняются, а также сохраняется расположение этойyield return
инструкции.yield return
Если оператор находится в одном или несколькихtry
блоках, связанные блоки finally не выполняются в этот момент. - Состояние объекта перечислителя изменяется на приостановленное.
- Метод
MoveNext
возвращаетtrue
вызывающей стороне, указывая, что итерация успешно перешла к следующему значению.
- Выражение, заданное в инструкции, вычисляется, неявно преобразуется в тип доходности и назначается
- При встрече с выражением
yield break
(§9.4.4.20):-
yield break
Если инструкция находится в одном или несколькихtry
блоках, выполняются связанныеfinally
блоки. - Состояние объекта перечислителя изменено на после.
- Метод
MoveNext
возвращаетсяfalse
вызывающему объекту, указывая, что итерация завершена.
-
- При достижении конца тела итератора:
- Состояние объекта перечислителя изменено на после.
- Метод
MoveNext
возвращаетсяfalse
вызывающему объекту, указывая, что итерация завершена.
- При возникновении исключения и его распространении за пределы блока итератора:
- Соответствующие
finally
блоки в теле итератора уже были выполнены распространением исключений. - Состояние объекта перечислителя изменено на после.
- Распространение исключения продолжается к вызывающему методу
MoveNext
.
- Соответствующие
15.15.5.3 Получение текущего значения
На свойство объекта Current
перечислителя влияют операторы yield return
в блоке итератора.
Примечание. Свойство
Current
является синхронным свойством как для синхронных, так и асинхронных итераторов объектов. конечная заметка
Если объект перечислителя находится в приостановленном состоянии, значением является значение Current
, заданное предыдущим вызовом MoveNext
. Если объект перечислителя находится в состояниях до выполнения, во время выполнения или после выполнения, результат обращения к Current
не определён.
Для итератора с типом возвращаемого результата, отличным от object
, результат доступа Current
через IEnumerable
реализации объекта перечислителя соответствует доступу к Current
через IEnumerator<T>
реализации объекта перечислителя и приведение результата к типу object
.
15.15.5.4 Удаление ресурсов
Используется метод Dispose
или DisposeAsync
для завершения итерации путем приведения объекта перечислителя в состояние после.
- Если состояние объекта перечислителя до, вызов
Dispose
изменяет состояние на после. - Если состояние объекта перечислителя — выполнение, результат вызова
Dispose
не указан. - Если состояние объекта перечислителя приостановлено, вызовите
Dispose
:- Изменяет состояние на работу.
- Выполняет все блоки finally так, как если бы последним выполненным оператором была инструкция
yield return
. Если это приводит к выбросу и распространению исключения из тела итератора, состояние объекта перечислителя устанавливается в после, и исключение передается вызывающему методуDispose
. - Изменяет состояние на after.
- Если объект перечислителя оказался в состоянии после, вызов
Dispose
не оказывает никакого эффекта.
15.15.6 Перечисление объектов
15.15.6.1 General
Если элемент функции или локальная функция, возвращающая тип перечисленного интерфейса, реализуется с помощью блока итератора, вызов элемента функции не сразу выполняет код в блоке итератора. Вместо этого создается и возвращается перечислимый объект .
Метод GetEnumerator
или GetAsyncEnumerator
объекта перечисления возвращает объект перечислителя, который инкапсулирует код, указанный в блоке итератора, и выполнение кода в блоке итератора происходит при вызове метода MoveNext
или MoveNextAsync
объекта перечислителя. Перечисленный объект имеет следующие характеристики:
- Он реализует
IEnumerable
иIEnumerable<T>
илиIAsyncEnumerable<T>
, гдеT
тип доходности итератора. - Он инициализирован с копией значений аргументов (если таковые имеются) и значения экземпляра, переданные члену функции.
Перечисляемый объект обычно является экземпляром класса, созданного компилятором, который инкапсулирует код в блоке итератора и реализует перечисляемые интерфейсы, но возможны другие методы реализации. Если перечисленный класс создается компилятором, этот класс будет вложен напрямую или косвенно в класс, содержащий член функции, он будет иметь частные специальные возможности, и он будет иметь имя, зарезервированное для использования компилятором (§6.4.3).
Перечисленный объект может реализовать больше интерфейсов, чем указанные выше.
Примечание. Например, перечисляемый объект может также реализовать
IEnumerator
иIEnumerator<T>
, что позволяет использовать его как перечисляемый, так и перечислитель. Как правило, такая реализация возвращает собственный экземпляр (для сохранения выделений памяти) при первом вызовеGetEnumerator
. Последующие вызовыGetEnumerator
, если таковые есть, возвращают новый экземпляр класса, как правило, одного класса, чтобы вызовы разных экземпляров перечислителя не влияли друг на друга. Он не может возвращать тот же самый экземпляр, даже если предыдущий перечислитель уже дочислил до конца последовательности, так как все последующие вызовы исчерпанного перечислителя должны приводить к выбросу исключений. конечная заметка
15.15.6.2 Метод GetEnumerator или GetAsyncEnumerator
Перечисляемый объект предоставляет реализацию GetEnumerator
методов IEnumerable
и IEnumerable<T>
интерфейсов. Два GetEnumerator
метода совместно используют общую реализацию, которая получает и возвращает доступный объект перечислителя. Объект перечислителя инициализируется со значениями аргументов и значением экземпляра, сохраненным при инициализации объекта перечисления, но в противном случае объект перечислителя работает, как описано в разделе §15.15.5.
Асинхронный объект с перечислением предоставляет реализацию GetAsyncEnumerator
метода IAsyncEnumerable<T>
интерфейса. Этот метод возвращает доступный объект асинхронного перечислителя. Объект перечислителя инициализируется со значениями аргументов и значением экземпляра, сохраненным при инициализации объекта перечисления, но в противном случае объект перечислителя работает, как описано в разделе §15.15.5.
ECMA C# draft specification