15 классов
15.1 Общие
Класс — это структура данных, которая может содержать элементы данных (константы и поля), члены функции (методы, свойства, события, индексаторы, операторы, конструкторы экземпляров, методы завершения и статические конструкторы) и вложенные типы. Типы классов поддерживают наследование, механизм, в котором производный класс может расширять и специализировать базовый класс.
Объявления классов 15.2
15.2.1 Общие
Class_declaration — это type_declaration (§14.7), которая объявляет новый класс.
class_declaration
: attributes? class_modifier* 'partial'? 'class' identifier
type_parameter_list? class_base? type_parameter_constraints_clause*
class_body ';'?
;
Class_declaration состоит из необязательного набора атрибутов(§22partial
), а затем ключевое слово и class
, который называет класс, за которым следует необязательный type_parameter_list (§15.2.3), за которым следует необязательное class_base спецификации (§15.2.4), за которым следует необязательный набор type_parameter_constraints_clause (§15.2.5), за которым следует class_body (§15.2.6), за которым следует точка с запятой.
Объявление класса не должно предоставлять type_parameter_constraints_clause, если оно также не предоставляет type_parameter_list.
Объявление класса, которое предоставляет type_parameter_list , является универсальным объявлением класса. Кроме того, любой класс, вложенный в объявление универсального класса или универсальное объявление структуры, является объявлением универсального класса, так как аргументы типов для содержащего типа должны быть предоставлены для создания созданного типа (§8.4).
Модификаторы классов 15.2.2
15.2.2.1 Общие
При необходимости class_declaration может содержать последовательность модификаторов классов:
class_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'abstract'
| 'sealed'
| 'static'
| unsafe_modifier // unsafe code support
;
unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).
Это ошибка во время компиляции для одного модификатора, который будет отображаться несколько раз в объявлении класса.
Модификатор new
разрешен для вложенных классов. Он указывает, что класс скрывает унаследованный член по тому же имени, как описано в §15.3.5. Это ошибка во время компиляции для модификатора, new
отображаемого в объявлении класса, которое не является вложенным объявлением класса.
Модификаторы public
и , protected
internal
private
а также управляют специальными возможностями класса. В зависимости от контекста, в котором происходит объявление класса, некоторые из этих модификаторов могут быть запрещены (§7.5.2).
Если объявление частичного типа (§15.2.7) включает спецификацию специальных возможностей (private
Если часть частичного типа не включает спецификацию специальных возможностей, тип получает соответствующую доступность по умолчанию (§7.5.2).
Модификаторы abstract
и sealed
модификаторы static
рассматриваются в следующих подклаузах.
15.2.2.2 Абстрактные классы
Модификатор abstract
используется для указания того, что класс является неполным и предназначен для использования только в качестве базового класса. Абстрактный класс отличается от не абстрактного класса следующим образом:
- Абстрактный класс нельзя создать непосредственно, и это ошибка во время компиляции для использования
new
оператора в абстрактном классе. Хотя можно иметь переменные и значения, типы времени компиляции которых являются абстрактными, такие переменные и значения обязательно будут либоnull
содержать ссылки на экземпляры не абстрактных классов, производных от абстрактных типов. - Абстрактный класс разрешен (но не требуется) для хранения абстрактных элементов.
- Абстрактный класс нельзя запечатывать.
Если не абстрактный класс является производным от абстрактного класса, не абстрактный класс должен включать фактические реализации всех унаследованных абстрактных элементов, тем самым переопределяя эти абстрактные члены.
Пример. В следующем коде
abstract class A { public abstract void F(); } abstract class B : A { public void G() {} } class C : B { public override void F() { // Actual implementation of F } }
Абстрактный класс
A
представляет абстрактный методF
. КлассB
вводит дополнительный методG
, но так как он не предоставляет реализациюF
,B
также должен быть объявлен абстрактным. КлассC
переопределяетF
и предоставляет фактическую реализацию. Поскольку в ней нет абстрактных элементовC
,C
разрешено (но не обязательно) быть не абстрактными.пример конца
Если одна или несколько частей объявления частичного типа (§15.2.7) класса включают abstract
модификатор, класс является абстрактным. В противном случае класс является не абстрактным.
15.2.2.3 Запечатанные классы
Модификатор sealed
используется для предотвращения производных от класса. Ошибка во время компиляции возникает, если запечатанный класс указан как базовый класс другого класса.
Запечатанный класс также не может быть абстрактным классом.
Примечание.
sealed
Модификатор в основном используется для предотвращения непреднамеренного производных, но также включает определенные оптимизации во время выполнения. В частности, поскольку закрытый класс, как известно, никогда не имеет производных классов, можно преобразовать вызовы виртуальных функций в запечатанные экземпляры классов в не виртуальные вызовы. конечная заметка
Если одна или несколько частей объявления частичного типа (§15.2.7) класса включают sealed
модификатор, класс запечатывается. В противном случае класс неуправляем.
Статические классы 15.2.2.4
15.2.2.4.1 Общие
Модификатор static
используется для обозначения класса, объявленного как статический класс. Статический класс не должен быть создан, не должен использоваться в качестве типа и содержать только статические элементы. Только статический класс может содержать объявления методов расширения (§15.6.10).
Объявление статического класса имеет следующие ограничения:
- Статический
sealed
класс не должен включать илиabstract
модификатор. (Однако, поскольку статический класс не может быть создан или производный от него, он ведет себя так, как если бы он был запечатан и абстрагирован.) - Статический класс не должен содержать спецификацию class_base (§15.2.4) и не может явно указывать базовый класс или список реализованных интерфейсов. Статический класс неявно наследует от типа
object
. - Статический класс должен содержать только статические элементы (§15.3.8).
Примечание. Все константы и вложенные типы классифицируются как статические элементы. конечная заметка
- Статический класс не должен содержать члены со
protected
специальнымиprivate protected
возможностями илиprotected internal
объявленными специальными возможностями.
Это ошибка во время компиляции для нарушения любого из этих ограничений.
Статический класс не имеет конструкторов экземпляров. Не удается объявить конструктор экземпляра в статичном классе, и для статического класса конструктор экземпляра по умолчанию (§15.11.5) не предоставляется для статического класса.
Члены статического класса не являются автоматически статическими, а объявления-члены должны явно включать static
модификатор (за исключением констант и вложенных типов). Если класс вложен в статический внешний класс, вложенный класс не является статическим, если он явно не включает static
модификатор.
Если одна или несколько частей объявления частичного типа (§15.2.7) класса включают static
модификатор, класс является статическим. В противном случае класс не является статическим.
15.2.2.4.2, ссылающиеся на типы статических классов
Namespace_or_type_name (§7.8) разрешено ссылаться на статический класс, если
- Namespace_or_type_name — это
T
namespace_or_type_name формыT.I
или - Имя namespace_or_type — это
T
typeof_expression (§12.8.18) формыtypeof(T)
.
Primary_expression (§12.8) разрешено ссылаться на статический класс, если
- Primary_expression представляет собой
E
member_access (§12.8.7) формыE.I
.
В любом другом контексте это ошибка во время компиляции для ссылки на статический класс.
Примечание. Например, это ошибка для статического класса, используемого в качестве базового класса, составного типа (§15.3.7) элемента, аргумента универсального типа или ограничения параметра типа. Аналогичным образом статический класс нельзя использовать в типе массива, новом выражении, выражении приведения, выражением как выражением, выражением как выражением,
sizeof
выражением или выражением значения по умолчанию. конечная заметка
Параметры типа 15.2.3
Параметр типа — это простой идентификатор, обозначающий заполнитель для аргумента типа, предоставленного для создания созданного типа. По ограничению аргумент типа (§8.4.2) является типом, заменяемым параметром типа при создании созданного типа.
type_parameter_list
: '<' type_parameters '>'
;
type_parameters
: attributes? type_parameter
| type_parameters ',' attributes? type_parameter
;
type_parameter определен в §8.5.
Каждый параметр типа в объявлении класса определяет имя в пространстве объявления (§7.3) этого класса. Таким образом, он не может иметь то же имя, что и другой параметр типа этого класса или член, объявленный в этом классе. Параметр типа не может иметь то же имя, что и сам тип.
Два частичных объявления универсального типа (в одной программе) способствуют одному и тому же универсальному типу, если у них есть одно полное имя (которое включает generic_dimension_specifier (§12.8.18) для числа параметров типа) (§7.8.3). Два таких объявления частичных типов должны указывать одинаковое имя для каждого параметра типа в порядке.
Базовая спецификация класса 15.2.4
15.2.4.1 Общие
Объявление класса может включать спецификацию class_base , которая определяет прямой базовый класс класса и интерфейсы (§18) непосредственно реализованные классом.
class_base
: ':' class_type
| ':' interface_type_list
| ':' class_type ',' interface_type_list
;
interface_type_list
: interface_type (',' interface_type)*
;
Базовые классы 15.2.4.2
Если class_type включен в class_base, он указывает прямой базовый класс объявленного класса. Если объявление не частичного класса не имеет class_base или если class_base перечисляет только типы интерфейсов, предполагается, что object
прямой базовый класс. Если объявление частичного класса содержит спецификацию базового класса, спецификация базового класса должна ссылаться на тот же тип, что и все остальные части этого частичного типа, включающие спецификацию базового класса. Если часть частичного класса не включает спецификацию базового класса, базовый класс .object
Класс наследует элементы от своего прямого базового класса, как описано в §15.3.4.
Пример. В следующем коде
class A {} class B : A {}
Класс
A
, как говорят, является прямым базовым классомB
, иB
, как говорят, является производным отA
. ПосколькуA
явно не указывает прямой базовый класс, его прямой базовый класс неявноobject
.пример конца
Для созданного типа класса, включая вложенный тип, объявленный в объявлении универсального типа (§15.3.9.7), если базовый класс указан в объявлении универсального класса, базовый класс созданного типа получается путем подстановки для каждого type_parameter в объявлении базового класса, соответствующего type_argument созданного типа.
Пример. Учитывая объявления универсального класса
class B<U,V> {...} class G<T> : B<string,T[]> {...}
базовый класс созданного типа
G<int>
будетB<string,int[]>
.пример конца
Базовый класс, указанный в объявлении класса, может быть созданным типом класса (§8.4). Базовый класс не может быть параметром типа самостоятельно (§8.5), хотя он может включать параметры типа, которые находятся в области.
Пример:
class Base<T> {} // Valid, non-constructed class with constructed base class class Extend1 : Base<int> {} // Error, type parameter used as base class class Extend2<V> : V {} // Valid, type parameter used as type argument for base class class Extend3<V> : Base<V> {}
пример конца
Прямой базовый класс типа класса должен быть по крайней мере таким же доступным, как сам тип класса (§7.5.5). Например, это ошибка во время компиляции для общедоступного класса, наследуемого от частного или внутреннего класса.
Прямой базовый класс типа класса не должен иметь ни одного из следующих типов: System.Array
, System.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_clauses
: type_parameter_constraints_clause
| type_parameter_constraints_clauses type_parameter_constraints_clause
;
type_parameter_constraints_clause
: 'where' type_parameter ':' type_parameter_constraints
;
type_parameter_constraints
: primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
| secondary_constraints (',' constructor_constraint)?
| constructor_constraint
;
primary_constraint
: class_type nullable_type_annotation?
| 'class' nullable_type_annotation?
| 'struct'
| 'notnull'
| 'unmanaged'
;
secondary_constraint
: interface_type nullable_type_annotation?
| type_parameter nullable_type_annotation?
;
secondary_constraints
: secondary_constraint (',' secondary_constraint)*
;
constructor_constraint
: 'new' '(' ')'
;
Каждый type_parameter_constraints_clause состоит из маркера where
, за которым следует имя параметра типа, за которым следует двоеточие и список ограничений для этого параметра типа. Для каждого параметра типа может быть не более одного where
предложения, и where
предложения могут быть перечислены в любом порядке.
get
Как и set
маркеры в методе доступа к свойствам, where
маркер не является ключевым словом.
Список ограничений, указанных в where
предложении, может включать в себя любой из следующих компонентов: одно основное ограничение, одно или несколько дополнительных ограничений и ограничение конструктора. new()
Основное ограничение может быть типом класса, unmanaged
Тип класса и ограничение ссылочного типа могут включать nullable_type_annotation.
Дополнительное ограничение может быть interface_type или type_parameter, за которым следует nullable_type_annotation. Наличие nullable_type_annotatione* указывает, что аргумент типа может быть ссылочным типом, допускающим значение NULL, который соответствует типу ссылок, не допускающим значения NULL, который удовлетворяет ограничению.
Ограничение ссылочного типа указывает, что аргумент типа, используемый для параметра типа, должен быть ссылочным типом. Все типы классов, типы интерфейсов, типы делегатов, типы массивов и параметры типа, известные как ссылочный тип (как определено ниже), удовлетворяют этому ограничению.
Тип класса, ограничение ссылочного типа и вторичные ограничения могут включать заметку типа NULL. Наличие или отсутствие этой заметки в параметре типа указывает ожидания null для аргумента типа:
- Если ограничение не включает заметку типа null, аргумент типа, как ожидается, будет не допускающим значение NULL ссылочным типом. Компилятор может выдавать предупреждение, если аргумент типа является ссылочным типом, допускающим значение NULL.
- Если ограничение включает заметку типа NULL, ограничение удовлетворяется как типом ссылок, не допускающим значение NULL, так и ссылочным типом, допускающим значение NULL.
Значение NULL аргумента типа не совпадает со значением NULL параметра типа. Компилятор может выдавать предупреждение, если значение NULL параметра типа не соответствует значению NULL аргумента типа.
Примечание. Чтобы указать, что аргумент типа является ссылочным типом, допускающим значение NULL, не добавляйте заметку типа NULL в качестве ограничения (используйте
T : class
илиT : BaseClass
), но используйтеT?
в универсальном объявлении для указания соответствующего ссылочного типа, допускающего значение NULL, для аргумента типа. конечная заметка
Заметка типа, допускающего значение NULL, ?
не может использоваться для аргумента неограниченного типа.
Для параметра T
типа, если аргумент типа является ссылочным типом C?
, допускающего значение NULL, экземпляры T?
интерпретируются как C?
, а не C??
.
Пример. В следующих примерах показано, как значение NULL аргумента типа влияет на значение NULL объявления его параметра типа:
public class C { } public static class Extensions { public static void M<T>(this T? arg) where T : notnull { } } public class Test { public void M() { C? mightBeNull = new C(); C notNull = new C(); int number = 5; int? missing = null; mightBeNull.M(); // arg is C? notNull.M(); // arg is C? number.M(); // arg is int? missing.M(); // arg is int? } }
Если аргумент типа является типом, не допускающим значение NULL,
?
заметка типа указывает, что параметр является соответствующим типом, допускающим значение NULL. Если аргумент типа уже является ссылочным типом, допускающим значение NULL, параметр является тем же типом, допускающим значение NULL.пример конца
Ограничение null указывает, что аргумент типа, используемый для параметра типа, должен быть типом ненулевого значения или не допускающим значение null типом ссылок. Аргумент типа, который не является ненулевым типом значения или ненулевым ссылочным типом, допускается, но компилятор может выдать диагностическое предупреждение.
Ограничение типа значения указывает, что аргумент типа, используемый для параметра типа, должен быть типом значения, не допускающим значение NULL. Все типы структур, не допускающие значения NULL, типы перечисления и параметры типа, имеющие ограничение типа значения, удовлетворяют этому ограничению. Обратите внимание, что хотя классифицируется как тип значения, тип значения null (§8.3.12) не удовлетворяет ограничению типа значения. Параметр типа с ограничением типа не должен также иметь constructor_constraint, хотя он может использоваться в качестве аргумента типа для другого параметра типа с constructor_constraint.
Примечание. Тип
System.Nullable<T>
задает ограничение типа значения, не допускающего значение NULL, дляT
. Таким образом, рекурсивно построенные типы формT??
иNullable<Nullable<T>>
запрещены. конечная заметка
Поскольку unmanaged
это не ключевое слово, в primary_constraint неуправляемое ограничение всегда синтаксически неоднозначно с class_type. По соображениям совместимости, если подстановка имени (§12.8.4) имени unmanaged
будет выполнена class_type
успешно. В противном случае оно рассматривается как неуправляемые ограничения.
Ограничение неуправляемого типа указывает, что аргумент типа, используемый для параметра типа, должен быть неуправляемым типом (§8.8).
Типы указателей никогда не могут быть аргументами типа и не удовлетворяют ни одному ограничению типа, даже неуправляемому, несмотря на неуправляемые типы.
Если ограничение является типом класса, типом интерфейса или параметром типа, этот тип указывает минимальный базовый тип, который должен поддерживать каждый аргумент типа, используемый для этого параметра типа. При использовании созданного типа или универсального метода аргумент типа проверяется на соответствие ограничениям параметра типа во время компиляции. Указанный аргумент типа должен соответствовать условиям, описанным в разделе 8.4.5.
Ограничение class_type должно соответствовать следующим правилам:
- Тип должен быть типом класса.
- Тип не должен быть
sealed
. - Тип не должен быть одним из следующих типов:
System.Array
илиSystem.ValueType
. - Тип не должен быть
object
. - По крайней мере одно ограничение для заданного параметра типа может быть типом класса.
Тип, указанный в качестве ограничения interface_type , должен соответствовать следующим правилам:
- Тип должен быть типом интерфейса.
- Тип не должен указываться несколько раз в заданном
where
предложении.
В любом случае ограничение может включать любой из параметров типа связанного типа или объявления метода в составе созданного типа и может включать объявленный тип.
Любой класс или тип интерфейса, указанный в качестве ограничения параметра типа, должен быть по крайней мере доступным (§7.5.5) как объявленный универсальный тип или метод.
Тип, указанный как ограничение type_parameter , должно соответствовать следующим правилам:
- Тип должен быть параметром типа.
- Тип не должен указываться несколько раз в заданном
where
предложении.
Кроме того, в граф зависимостей параметров типа не должно быть циклов, в которых зависимость является транзитивной связью, определенной следующим образом:
- Если параметр
T
типа используется в качестве ограничения для параметраS
типа, тоS
зависит отT
него. - Если параметр
S
типа зависит от параметраT
типа иT
зависит от параметра типа, тоS
негоU
.
Учитывая это отношение, это ошибка во время компиляции для параметра типа, который будет зависеть от себя (прямо или косвенно).
Все ограничения должны быть согласованы между зависимыми параметрами типа. Если параметр S
типа зависит от параметра T
типа, то:
-
T
не должно иметь ограничения типа значения. В противном случае фактически запечатанный,T
поэтомуS
он должен быть таким же типом, какT
и при устранении необходимости двух параметров типа. - Если
S
имеет ограничение типа значения,T
то не должно быть ограничения class_type . - Если
S
имеет ограничение class_type иA
имеетB
, то должно быть преобразование удостоверений или неявное преобразование ссылок изA
B
или неявное преобразование ссылок изB
A
него. - Если
S
также зависит от параметраU
типа иU
имеет ограничение class_type иA
имеетT
B
, то должно быть преобразование удостоверений или неявное преобразование ссылок изA
B
или неявное преобразование ссылок в .B
A
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
.
Каждая часть объявления частичного типа должна включать partial
модификатор и должна быть объявлена в том же пространстве имен или содержит тип, что и другие части. Модификатор partial
указывает, что дополнительные части объявления типа могут существовать в другом месте, но существование таких дополнительных частей не является обязательным требованием; оно допустимо для единственного объявления типа для включения partial
модификатора. Допускается только одно объявление частичного типа, включающее базовый класс или реализованные интерфейсы. Однако все объявления базового класса или реализованные интерфейсы должны соответствовать, включая допустимость значений NULL для всех указанных аргументов типа.
Все части частичного типа должны компилироваться вместе, чтобы части могли быть объединены во время компиляции. Частичные типы, в частности, не позволяют расширить уже скомпилированные типы.
Вложенные типы можно объявить в нескольких частях с помощью partial
модификатора. Как правило, содержащий тип объявляется также с помощью partial
, и каждая часть вложенного типа объявляется в другой части содержащего типа.
Пример. Следующий частичный класс реализуется в двух частях, которые находятся в разных единицах компиляции. Первая часть — это компьютер, созданный средством сопоставления баз данных, а вторая часть создается вручную:
public partial class Customer { private int id; private string name; private string address; private List<Order> orders; public Customer() { ... } } // File: Customer2.cs public partial class Customer { public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted); public bool HasOutstandingOrders() => orders.Count > 0; }
При компиляции двух частей выше результирующий код ведет себя так, как если бы класс был написан в виде одной единицы, как показано ниже.
public class Customer { private int id; private string name; private string address; private List<Order> orders; public Customer() { ... } public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted); public bool HasOutstandingOrders() => orders.Count > 0; }
пример конца
Обработка атрибутов, указанных в параметрах типа или типа различных частей частичного объявления, рассматривается в §22.3.
Члены класса 15.3
15.3.1 Общие
Члены класса состоят из элементов, введенных его class_member_declarationи членов, унаследованных от прямого базового класса.
class_member_declaration
: constant_declaration
| field_declaration
| method_declaration
| property_declaration
| event_declaration
| indexer_declaration
| operator_declaration
| constructor_declaration
| finalizer_declaration
| static_constructor_declaration
| type_declaration
;
Члены класса делятся на следующие категории:
- Константы, представляющие значения констант, связанные с классом (§15.4).
- Поля, которые являются переменными класса (§15.5).
- Методы, реализующие вычисления и действия, которые могут выполняться классом (§15.6).
- Свойства, определяющие именованные характеристики и действия, связанные с чтением и записью этих характеристик (§15.7).
- События, определяющие уведомления, которые могут быть созданы классом (§15.8).
- Индексаторы, которые позволяют индексировать экземпляры класса таким же образом (синтаксически) как массивы (§15.9).
- Операторы, определяющие операторы выражений, которые могут применяться к экземплярам класса (§15.10).
- Конструкторы экземпляров, реализующие действия, необходимые для инициализации экземпляров класса (§15.11)
- Методы завершения, реализующие действия, выполняемые до окончательного удаления экземпляров класса (§15.13).
- Статические конструкторы, реализующие действия, необходимые для инициализации самого класса (§15.12).
- Типы, представляющие типы, которые являются локальными для класса (§14.7).
Class_declaration создает новое пространство объявления (§7.3), а type_parameter и class_member_declarationсразу же содержатся в class_declaration в этом пространстве объявлений. Следующие правила применяются к class_member_declarations:
Конструкторы экземпляров, методы завершения и статические конструкторы должны иметь то же имя, что и немедленно заключенный класс. Все остальные члены должны иметь имена, отличающиеся от имени немедленно заключенного класса.
Имя параметра типа в type_parameter_list объявления класса должно отличаться от имен всех остальных параметров типа в том же type_parameter_list и должно отличаться от имени класса и имен всех членов класса.
Имя типа должно отличаться от имен всех членов, не относящихся к типу, объявленных в одном классе. Если два или более объявлений типов имеют одинаковое полное имя, объявления должны иметь
partial
модификатор (§15.2.7) и эти объявления объединяются для определения одного типа.
Примечание. Так как полное имя объявления типа кодирует число параметров типа, два разных типа могут совместно использовать то же имя, если они имеют другое число параметров типа. конечная заметка
Имя константы, поля, свойства или события должно отличаться от имен всех остальных членов, объявленных в одном классе.
Имя метода должно отличаться от имен всех других не-методов, объявленных в одном классе. Кроме того, подпись (§7.6) метода должна отличаться от сигнатур всех других методов, объявленных в одном классе, и два метода, объявленные в одном классе, не должны иметь сигнатуры, которые отличаются исключительно по
in
,out
иref
.Подпись конструктора экземпляра должна отличаться от подписей всех остальных конструкторов экземпляров, объявленных в одном классе, и два конструктора, объявленные в одном классе, не должны иметь сигнатуры, которые отличаются исключительно по
ref
иout
.Подпись индексатора должна отличаться от подписей всех остальных индексаторов, объявленных в одном классе.
Подпись оператора должна отличаться от подписей всех остальных операторов, объявленных в одном классе.
Унаследованные члены класса (§15.3.4) не являются частью пространства объявления класса.
Примечание. Таким образом, производный класс может объявлять член с тем же именем или сигнатурой, что и унаследованный элемент (который в действительности скрывает унаследованный элемент). конечная заметка
Набор членов типа, объявленного в нескольких частях (§15.2.7), является объединением членов, объявленных в каждой части. Тела всех частей объявления типа разделяют одно и то же пространство объявления (§7.3), а область каждого элемента (§7.7) распространяется на тела всех частей. Домен специальных возможностей любого члена всегда включает все части заключенного типа; Частный член, объявленный в одной части, свободно доступен из другой части. Это ошибка во время компиляции для объявления одного элемента в нескольких части типа, если этот элемент не является типом с модификатором partial
.
Пример:
partial class A { int x; // Error, cannot declare x more than once partial class Inner // Ok, Inner is a partial type { int y; } } partial class A { int x; // Error, cannot declare x more than once partial class Inner // Ok, Inner is a partial type { int z; } }
пример конца
Порядок инициализации полей может быть значительным в коде C#, а некоторые гарантии предоставляются, как определено в разделе 15.5.6.1. В противном случае порядок элементов в типе редко важен, но может быть значительным при взаимодействии с другими языками и средами. В таких случаях порядок элементов в типе, объявленном в нескольких частях, не определен.
15.3.2 Тип экземпляра
Каждое объявление класса имеет связанный тип экземпляра. Для объявления универсального класса тип экземпляра формируется путем создания созданного типа (§8.4) из объявления типа, причем каждый из указанных аргументов типа является соответствующим параметром типа. Так как тип экземпляра использует параметры типа, его можно использовать только в том месте, где параметры типа находятся в области; то есть внутри объявления класса. Тип экземпляра — это тип кода, написанного this
внутри объявления класса. Для не универсальных классов тип экземпляра — это просто объявленный класс.
Пример. Ниже показаны несколько объявлений классов, а также их типы экземпляров:
class A<T> // instance type: A<T> { class B {} // instance type: A<T>.B class C<U> {} // instance type: A<T>.C<U> } class D {} // instance type: D
пример конца
15.3.3 Члены созданных типов
Ненаследуемые элементы созданного типа получаются путем подстановки для каждого type_parameter в объявлении члена, соответствующего type_argument созданного типа. Процесс подстановки основан на семантическом значении объявлений типов и не просто подстановки текста.
Пример. Учитывая объявление универсального класса
class Gen<T,U> { public T[,] a; public void G(int i, T t, Gen<U,T> gt) {...} public U Prop { get {...} set {...} } public int H(double d) {...} }
Созданный тип
Gen<int[],IComparable<string>>
имеет следующие элементы:public int[,][] a; public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...} public IComparable<string> Prop { get {...} set {...} } public int H(double d) {...}
Тип элемента в объявлении
a
универсального класса — "двухмерный массивGen
", поэтому тип элементаT
в созданном выше типе — "двухмерный массив одномерного массиваa
", илиint
.int[,][]
пример конца
В членах функции экземпляра тип экземпляра this
— это тип экземпляра (§15.3.2) содержащего объявления.
Все члены универсального класса могут использовать параметры типа из любого заключающего класса напрямую или в составе созданного типа. При использовании определенного закрытого созданного типа (§8.4.3) во время выполнения каждое использование параметра типа заменяется аргументом типа, предоставленным созданному типу.
Пример:
class C<V> { public V f1; public C<V> f2; public C(V x) { this.f1 = x; this.f2 = this; } } class Application { static void Main() { C<int> x1 = new C<int>(1); Console.WriteLine(x1.f1); // Prints 1 C<double> x2 = new C<double>(3.1415); Console.WriteLine(x2.f1); // Prints 3.1415 } }
пример конца
Наследование 15.3.4
Класс наследует члены своего прямого базового класса. Наследование означает, что класс неявно содержит все члены своего прямого базового класса, за исключением конструкторов экземпляров, завершения и статических конструкторов базового класса. Ниже приведены некоторые важные аспекты наследования:
Наследование является транзитивным. Если
C
он является производным отB
, иB
является производнымA
, тоC
наследует члены, объявленные вB
, а также члены, объявленные вA
.Производный класс расширяет его прямой базовый класс. Производный класс может дополнить наследуемые элементы новыми элементами, но он не может удалить определение для наследуемого члена.
Конструкторы экземпляров, методы завершения и статические конструкторы не наследуются, но все остальные члены являются независимо от объявленной специальных возможностей (§7.5). Однако в зависимости от объявленной доступности унаследованные члены могут быть недоступны в производном классе.
Производный класс может скрыть (§7.7.2.3), наследуя элементы, объявляя новые члены с тем же именем или подписью. Однако скрытие унаследованного элемента не удаляет этот элемент— он просто делает этот элемент недоступным непосредственно через производный класс.
Экземпляр класса содержит набор всех полей экземпляров, объявленных в классе и его базовых классах, а неявное преобразование (§10.2.8) существует из производного типа класса в любой из его типов базовых классов. Таким образом, ссылка на экземпляр определенного производного класса может рассматриваться как ссылка на экземпляр любого из его базовых классов.
Класс может объявлять виртуальные методы, свойства, индексаторы и события, а производные классы могут переопределить реализацию этих элементов функции. Это позволяет классам проявлять полиморфное поведение, в котором действия, выполняемые вызовом члена функции, зависят от типа времени выполнения экземпляра, через который вызывается этот элемент функции.
Наследуемые элементы созданного типа класса являются членами немедленного типа базового класса (§15.2.4.2), который найден путем замены аргументов типа созданного типа для каждого вхождения соответствующих параметров типа в base_class_specification. Эти члены, в свою очередь, преобразуются путем подстановки для каждого type_parameter в объявлении члена, соответствующего type_argument base_class_specification.
Пример:
class B<U> { public U F(long index) {...} } class D<T> : B<T[]> { public T G(string s) {...} }
В приведенном выше коде созданный тип
D<int>
имеет открытый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) имеет собственный набор статических полей независимо от количества экземпляров закрытого созданного типа.
- Статический член функции (метод, свойство, событие, оператор или конструктор) не работает с определенным экземпляром, и это ошибка во время компиляции, ссылающейся на это в таком элементе функции.
Если поле, метод, свойство, событие, индексатор, конструктор или объявление завершения не включает статический модификатор, он объявляет элемент экземпляра. (Иногда элемент экземпляра называется нестатичным элементом.) Члены экземпляра имеют следующие характеристики:
- Если элемент
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
объявлять специальные возможности по умолчанию. Вложенные типы также могут иметь эти формы объявленной специальных возможностей, а также одну или несколько дополнительных форм объявленной специальных возможностей в зависимости от того, является ли содержащий тип классом или структурой:
- Вложенный тип, объявленный в классе, может иметь любой из разрешенных типов объявленных специальных возможностей и, как и другие члены
private
класса, по умолчанию объявленные специальные возможности. - Вложенный тип, объявленный в структуре, может иметь любую из трех форм объявленной специальных возможностей (
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.4new
§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'
;
Constant_declaration может включать набор атрибутов(§22new
) и любой из разрешенных видов объявленной доступности (§15.3.6). Атрибуты и модификаторы применяются ко всем элементам, объявленным constant_declaration. Несмотря на то что константы считаются статическими элементами, constant_declaration не требует и не разрешает static
модификатор. Это ошибка для одного модификатора несколько раз в объявлении константы.
Тип constant_declaration указывает тип элементов, введенных объявлением. За типом следует список constant_declarator s (§13.6.3), каждый из которыхпредставляет новый элемент.
Constant_declarator состоит из идентификатора, который называет элемент, а затем маркером "=
" и constant_expression (§12.23), который дает значение элемента.
Тип, указанный в объявлении константы, должен быть , sbyte
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.6) и static
модификатор (§15.5.2). Кроме того, field_declaration может включать модификатор (§15.5.3volatile
), но не оба. Атрибуты и модификаторы применяются ко всем элементам, объявленным field_declaration. Это ошибка для одного модификатора несколько раз в field_declaration.
Тип field_declaration указывает тип элементов, введенных объявлением. За типом следует список variable_declarators, каждый из которых представляет новый член. Variable_declarator состоит из идентификатора, именующего этот член, а также маркера "=
" и variable_initializer (§15.5.6), который дает начальное значение этого элемента.
Тип поля должен быть по крайней мере таким же доступным, как сам поле (§7.5.5).
Значение поля получается в выражении с помощью simple_name (§12.8.4), member_access (§12.8.7) или base_access (§12.8.15). Значение поля без чтения изменяется с помощью назначения (§12.21). Значение нечитаемого поля может быть получено и изменено с помощью операторов добавочного и декремента постфикса (§12.8.16) и префиксов и добавочных и декрементных операторов (§12.9.6).
Объявление поля, объявляющее несколько полей, эквивалентно нескольким объявлениям отдельных полей с одинаковыми атрибутами, модификаторами и типом.
Пример:
class A { public static int X = 1, Y, Z = 100; }
эквивалентно правилу
class A { public static int X = 1; public static int Y; public static int Z = 100; }
пример конца
Поля статического и экземпляра 15.5.2
Если объявление поля включает static
модификатор, поля, представленные объявлением, являются статическими полями. Если модификатор отсутствует static
, поля, представленные объявлением, являются полями экземпляра. Статические поля и поля экземпляров являются двумя из нескольких типов переменных (§9), поддерживаемых C#, и иногда они называются статическими переменными и переменными экземпляра соответственно.
Как описано в §15.3.8, каждый экземпляр класса содержит полный набор полей экземпляра класса, в то время как существует только один набор статических полей для каждого не универсального класса или закрытого типа, независимо от количества экземпляров класса или закрытого созданного типа.
Поля чтения 15.5.3
15.5.3.1 Общие
Если field_declaration включает readonly
модификатор, поля, представленные объявлением, являются полями считывания. Прямые назначения для полей чтения могут возникать только в рамках этого объявления или в конструкторе экземпляра или статического конструктора в том же классе. (Поле чтения можно назначить несколько раз в этих контекстах.) В частности, прямые назначения в поле чтения разрешены только в следующих контекстах:
- В variable_declarator, в которой представлено поле (включая variable_initializer в объявлении).
- Для поля экземпляра в конструкторах экземпляра класса, содержащего объявление поля; для статического поля в статическом конструкторе класса, содержащего объявление поля. Это также единственные контексты, в которых допустимо передать поле чтения в качестве выходного или ссылочного параметра.
Попытка назначить поле чтения или передать его в качестве выходного или ссылочного параметра в любом другом контексте является ошибкой во время компиляции.
15.5.3.2 Использование статических полей чтения для констант
Статическое поле чтения полезно при необходимости символьного имени для константного значения, но если тип значения не допускается в объявлении констант или когда значение невозможно вычислить во время компиляции.
Пример. В следующем коде
public class Color { public static readonly Color Black = new Color(0, 0, 0); public static readonly Color White = new Color(255, 255, 255); public static readonly Color Red = new Color(255, 0, 0); public static readonly Color Green = new Color(0, 255, 0); public static readonly Color Blue = new Color(0, 0, 255); private byte red, green, blue; public Color(byte r, byte g, byte b) { red = r; green = g; blue = b; } }
Black
, ,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). Эти оптимизации могут выполняться компилятором, системой выполнения или оборудованием. Для переменных полей такие оптимизации переупорядочения ограничены:
- Чтение изменяющегося поля называется переменным чтением. Переменная чтение имеет "получение семантики"; т. е. это гарантируется до любых ссылок на память, которая возникает после нее в последовательности инструкций.
- Запись изменяющегося поля называется переменной записью. Переменная запись имеет "семантику выпуска"; То есть это гарантируется после всех ссылок на память до инструкции записи в последовательности инструкций.
Эти ограничения гарантируют, что все потоки будут видеть временные записи, выполняемые другим потоком, в порядке выполнения. Соответствующая реализация не требуется для предоставления единого общего порядка переменных записей, как показано во всех потоках выполнения. Тип изменяющегося поля должен быть одним из следующих:
- Reference_type.
- Type_parameter, который, как известно, является ссылочным типом (§15.2.5).
- Тип
byte
,sbyte
,short
ushort
int
uint
char
float
bool
System.IntPtr
или .System.UIntPtr
- Enum_type с типом
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
хранилище было видимым для основного потока послеfinished
считывать значение 0 из поляresult
.finished
Объявление какvolatile
поле предотвращает такое несоответствие.пример конца
Инициализация поля 15.5.5
Начальное значение поля, будь то статическое поле или поле экземпляра, является значением по умолчанию (§9.3) типа поля. Невозможно наблюдать за значением поля до того, как эта инициализация по умолчанию произошла, и поле таким образом никогда не является "неинициализированным".
Пример: пример
class Test { static bool b; int i; static void Main() { Test t = new Test(); Console.WriteLine($"b = {b}, i = {t.i}"); } }
выводятся следующие выходные данные
b = False, i = 0
так как
b
и оба автоматически инициализированыi
для значений по умолчанию.пример конца
Инициализаторы переменных 15.5.6
15.5.6.1 Общие
Объявления полей могут включать variable_initializers. Для статических полей инициализаторы переменных соответствуют операторам присваивания, выполняемым во время инициализации класса. Например, поля инициализаторы переменных соответствуют операторам присваивания, выполняемым при создании экземпляра класса.
Пример: пример
class Test { static double x = Math.Sqrt(2.0); int i = 100; string s = "Hello"; static void Main() { Test a = new Test(); Console.WriteLine($"x = {x}, i = {a.i}, s = {a.s}"); } }
выводятся следующие выходные данные
x = 1.4142135623730951, i = 100, s = Hello
Поскольку назначение
x
происходит, когда инициализаторыi
статических полей выполняются и присваиваются иs
возникают при выполнении инициализаторов полей экземпляра.пример конца
Инициализация значений по умолчанию, описанная в §15.5.5 , возникает для всех полей, включая поля с инициализаторами переменных. Таким образом, при инициализации класса все статические поля в этом классе сначала инициализированы в значения по умолчанию, а затем инициализаторы статических полей выполняются в текстовом порядке. Аналогичным образом, когда создается экземпляр класса, все поля экземпляров в этом экземпляре сначала инициализированы в значениях по умолчанию, а затем инициализаторы полей экземпляра выполняются в текстовом порядке. При наличии объявлений полей в нескольких объявлениях частичного типа для одного типа порядок частей не определен. Однако в каждой части инициализаторы полей выполняются в порядке.
Статические поля с инициализаторами переменных можно наблюдать в состоянии значения по умолчанию.
Пример: Однако это настоятельно не рекомендуется в стиле. Пример
class Test { static int a = b + 1; static int b = a + 1; static void Main() { Console.WriteLine($"a = {a}, b = {b}"); } }
демонстрирует это поведение. Несмотря на циклические
a
определения иb
, программа действительна. В результате выходные данныеa = 1, b = 2
поскольку статические поля
a
и инициализированыb
0
(значение по умолчанию дляint
) перед выполнением инициализаторов. При выполнении инициализатораa
значениеb
равно нулю и поэтомуa
инициализируется1
в . Когда инициализатор дляb
выполнения, значение объекта уже1
инициализировано и поэтому инициализированоb
в2
.пример конца
Инициализация статического поля 15.5.6.2
Инициализаторы статических полей класса соответствуют последовательности назначений, выполняемых в текстовом порядке, в котором они отображаются в объявлении класса (§15.5.6.1). В частичном классе значение "текстового порядка" указывается в §15.5.6.1. Если статический конструктор (§15.12) существует в классе, выполнение инициализаторов статических полей происходит непосредственно перед выполнением статического конструктора. В противном случае инициализаторы статических полей выполняются во время, зависящее от реализации, до первого использования статического поля этого класса.
Пример: пример
class Test { static void Main() { Console.WriteLine($"{B.Y} {A.X}"); } public static int F(string s) { Console.WriteLine(s); return 1; } } class A { public static int X = Test.F("Init A"); } class B { public static int Y = Test.F("Init B"); }
Может вывести либо выходные данные:
Init A Init B 1 1
или выходные данные:
Init B Init A 1 1
Поскольку выполнение инициализатора и инициализатора
X
инициализатораY
может происходить в любом порядке; они могут возникать только до ссылок на эти поля. Однако в примере:class Test { static void Main() { Console.WriteLine($"{B.Y} {A.X}"); } public static int F(string s) { Console.WriteLine(s); return 1; } } class A { static A() {} public static int X = Test.F("Init A"); } class B { static B() {} public static int Y = Test.F("Init B"); }
Выходные данные должны быть:
Init B Init A 1 1
поскольку правила выполнения статических конструкторов (как определено в §15.12) предоставляют, что статический конструктор (и, следовательно
B
,B
инициализаторы статических полей) должен выполняться передA
статическим конструктором и инициализаторами полей.пример конца
Инициализация поля экземпляра 15.5.6.3
Инициализаторы переменных поля экземпляра класса соответствуют последовательности назначений, которые выполняются сразу после входа в любой из конструкторов экземпляра (§15.11.3) этого класса. В частичном классе значение "текстового порядка" указывается в §15.5.6.1. Инициализаторы переменных выполняются в текстовом порядке, в котором они отображаются в объявлении класса (§15.5.6.1). Процесс создания и инициализации экземпляра класса описан далее в разделе §15.11.
Инициализатор переменных для поля экземпляра не может ссылаться на созданный экземпляр. Таким образом, это ошибка во время компиляции для ссылки this
на инициализатор переменной, так как это ошибка во время компиляции для инициализатора переменной для ссылки на любой член экземпляра через simple_name.
Пример. В следующем коде
class A { int x = 1; int y = x + 1; // Error, reference to instance member of this }
инициализатор переменных для
y
результатов ошибки во время компиляции, так как ссылается на элемент создаваемого экземпляра.пример конца
Методы 15.6
15.6.1 Общие
Метод — это член, реализующий вычисление или действие, которое может выполнять объект или класс. Методы объявляются с помощью method_declaration:
method_declaration
: attributes? method_modifiers return_type method_header method_body
| attributes? ref_method_modifiers ref_kind ref_return_type method_header
ref_method_body
;
method_modifiers
: method_modifier* 'partial'?
;
ref_kind
: 'ref'
| 'ref' 'readonly'
;
ref_method_modifiers
: ref_method_modifier*
;
method_header
: member_name '(' parameter_list? ')'
| member_name type_parameter_list '(' parameter_list? ')'
type_parameter_constraints_clause*
;
method_modifier
: ref_method_modifier
| 'async'
;
ref_method_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
return_type
: ref_return_type
| 'void'
;
ref_return_type
: type
;
member_name
: identifier
| interface_type '.' identifier
;
method_body
: block
| '=>' null_conditional_invocation_expression ';'
| '=>' expression ';'
| ';'
;
ref_method_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
Заметки грамматики:
- unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).
- при признании method_body, если применяются null_conditional_invocation_expression и альтернативные выражения, следует выбрать прежнее.
Примечание. Перекрытие и приоритет между альтернативами здесь исключительно для описательного удобства; правила грамматики можно разработать, чтобы удалить перекрытие. ANTLR и другие системы грамматики принимают то же удобство и поэтому method_body автоматически имеет указанную семантику. конечная заметка
Method_declaration может включать набор атрибутов (§22) и один из разрешенных видов объявленной доступности (§15.3.6), (new
), (§15.6.3), (§15.3), (virtual
), (override
), (§15.6.6sealed
§15.6.7), abstract
(extern
) и (async
) модификаторов.
Объявление имеет допустимое сочетание модификаторов, если все из следующих значений имеют значение true:
- Объявление включает допустимое сочетание модификаторов доступа (§15.3.6).
- Объявление не включает один и тот же модификатор несколько раз.
- Объявление включает в себя не более одного из следующих модификаторов:
static
,virtual
иoverride
. - Объявление включает в себя не более одного из следующих модификаторов:
new
иoverride
. - Если объявление включает
abstract
модификатор, объявление не содержит ни одного из следующих модификаторов:static
, ,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 объявления метода return-by-value или return-no-value указывает тип результата, если таковой имеется, возвращаемый методом. Только метод возвращаемого значения может включать partial
модификатор (§15.6.9). Если объявление включает модификатор, async
должен быть или метод возвращает по значению, а возвращаемый тип — void
задачи (§15.15.1).
Ref_return_type объявления метода return-by-ref указывает тип переменной, на которую ссылается variable_reference, возвращаемой методом.
Универсальный метод — это метод, объявление которого включает type_parameter_list. Это указывает параметры типа для метода. Необязательные type_parameter_constraints_clauseуказывают ограничения для параметров типа.
Универсальный method_declaration для явной реализации элемента интерфейса не должен иметь никаких type_parameter_constraints_clause; объявление наследует какие-либо ограничения от ограничений метода интерфейса.
Аналогичным образом объявление метода с override
модификатором не должно иметь никаких type_parameter_constraints_clause, а ограничения параметров типа метода наследуются от переопределенного виртуального метода.
Member_name указывает имя метода. Если метод не является явной реализацией члена интерфейса (§18.6.2), member_name является просто идентификатором.
Для явной реализации элемента интерфейса member_name состоит из interface_type, за которым следует ".
" и идентификатор. В этом случае объявление не должно содержать модификаторов, отличных от (возможно) extern
или async
.
Необязательный parameter_list задает параметры метода (§15.6.2).
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
.
Type_parameter метода находятся в области method_declaration и могут использоваться для формирования типов в пределах этой области вreturn_type или ref_return_type, method_body или ref_method_body и type_parameter_constraints_clause, но не в атрибутах.
Все параметры и параметры типа должны иметь разные имена.
Параметры метода 15.6.2
15.6.2.1 Общие
Параметры метода, если таковые имеются, объявляются parameter_list метода.
parameter_list
: fixed_parameters
| fixed_parameters ',' parameter_array
| parameter_array
;
fixed_parameters
: fixed_parameter (',' fixed_parameter)*
;
fixed_parameter
: attributes? parameter_modifier? type identifier default_argument?
;
default_argument
: '=' expression
;
parameter_modifier
: parameter_mode_modifier
| 'this'
;
parameter_mode_modifier
: 'ref'
| 'out'
| 'in'
;
parameter_array
: attributes? 'params' array_type identifier
;
Список параметров состоит из одного или нескольких параметров, разделенных запятыми, из которых только последний может быть parameter_array.
Fixed_parameter состоит из необязательных наборов атрибутов (§22); необязательныйin
, out
ref
или this
модификатор; тип, идентификатор и необязательный default_argument. Каждый fixed_parameter объявляет параметр заданного типа с заданным именем. Модификатор 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 должно быть одним из следующих:
- constant_expression
- выражение формы
new S()
, гдеS
является типом значения - выражение формы
default(S)
, гдеS
является типом значения
Выражение должно быть неявно преобразовано удостоверением или преобразуемым значением NULL в тип параметра.
Если необязательные параметры происходят в объявлении частичного метода (§15.6.9), явная реализация члена интерфейса (§18.6.2), объявление индексатора с одним параметром (§3.15.9 или в объявлении оператора (§15.10.1) компилятор должен дать предупреждение, так как эти члены никогда не могут вызываться таким образом, чтобы аргументы были опущены.
Parameter_array состоит из необязательного набора атрибутов (§22), params
модификатора, array_type и идентификатора. Массив параметров объявляет один параметр заданного типа массива с заданным именем.
Array_type массива параметров должен быть одномерным типом массива (§17.2). В вызове метода массив параметров разрешает указывать один аргумент заданного типа массива, или позволяет указывать ноль или больше аргументов типа элемента массива. Массивы параметров описаны далее в разделе §15.6.2.4.
Parameter_array может возникать после необязательного параметра, но не может иметь значение по умолчанию — пропуск аргументов для parameter_array вместо этого приведет к созданию пустого массива.
Пример: ниже показаны различные виды параметров:
void M<T>( ref int i, decimal d, bool b = false, bool? n = false, string s = "Hello", object o = null, T t = default(T), params int[] a ) { }
В parameter_list для
M
,i
является обязательным параметром,ref
является обязательнымd
параметром значения,b
иs
o
t
являются необязательными параметрами значения иa
является массивом параметров.пример конца
Объявление метода создает отдельное пространство объявления (§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
).
Если параметр является параметром по ссылке, соответствующий аргумент в вызове метода должен состоять из соответствующего ключевого слова, in
ref
или, за out
которым следует variable_reference (§9.5) того же типа, что и параметр. Однако, если параметр является параметром in
, аргумент может быть выражением , для которого существует неявное преобразование (§10.2) из этого выражения аргумента в тип соответствующего параметра.
Ссылочные параметры не допускаются для функций, объявленных как итератор (§15.14) или асинхронной функции (§15.15).
В методе, который принимает несколько параметров по ссылке, можно представить одно и то же расположение хранилища.
Входные параметры 15.6.2.3.2
Параметр, объявленный модификаторомin
, является входным параметром. Аргумент, соответствующий входной параметру, является переменной, существующей в точке вызова метода, или одной из них, созданной реализацией (§12.6.2.3) в вызове метода. Правила определенного назначения см. в разделе "9.2.8".
Это ошибка во время компиляции для изменения значения входного параметра.
Примечание. Основная цель входных параметров — это эффективность. Если тип параметра метода является большой структурой (с точки зрения требований к памяти), полезно избежать копирования всего аргумента при вызове метода. Входные параметры позволяют методам ссылаться на существующие значения в памяти, обеспечивая защиту от нежелательных изменений этих значений. конечная заметка
Параметры ссылки 15.6.2.3.3
Параметр, объявленный модификатором, является ссылочным параметромref
. Правила определенного назначения см. в разделе "9.2.6".
Пример: пример
class Test { static void Swap(ref int x, ref int y) { int temp = x; x = y; y = temp; } static void Main() { int i = 1, j = 2; Swap(ref i, ref j); Console.WriteLine($"i = {i}, j = {j}"); } }
выводятся следующие выходные данные
i = 2, j = 1
Для вызова
Swap
вMain
,x
представляетi
иy
представляетj
. Таким образом, вызов имеет эффект переключения значенийi
иj
.пример конца
Пример. В следующем коде
class A { string s; void F(ref string a, ref string b) { s = "One"; a = "Two"; b = "Three"; } void G() { F(ref s, ref s); } }
Вызов invocation
F
G
передает ссылкуs
на обаa
иb
. Таким образом, для этого вызова именаs
a
и все относятся к одному расположению хранилища, аb
три назначения изменяют полеs
экземпляра.пример конца
struct
Для типа в методе экземпляра метод доступа экземпляра (§12.2.1this
§12.8.14).
Параметры вывода 15.6.2.3.4
Параметр, объявленный модификатором, является выходным out
параметром. Правила определенного назначения см. в разделе "9.2.7".
Метод, объявленный как частичный метод (§15.6.9), не должен иметь выходных параметров.
Примечание. Выходные параметры обычно используются в методах, которые создают несколько возвращаемых значений. конечная заметка
Пример:
class Test { static void SplitPath(string path, out string dir, out string name) { int i = path.Length; while (i > 0) { char ch = path[i - 1]; if (ch == '\\' || ch == '/' || ch == ':') { break; } i--; } dir = path.Substring(0, i); name = path.Substring(i); } static void Main() { string dir, name; SplitPath(@"c:\Windows\System\hello.txt", out dir, out name); Console.WriteLine(dir); Console.WriteLine(name); } }
Пример создает выходные данные:
c:\Windows\System\ hello.txt
Обратите внимание, что
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
, пока в заданном типе базового класса по крайней мере один доступный метод находится с той же подписью, что M
и после подстановки аргументов типа. В целях определения переопределенного базового метода метод считается доступным, если он имеет public
значение , если он имеет значение , если он protected
есть, protected internal
или internal
если он либо или private protected
объявлен в той же программе, что C
и .
Ошибка во время компиляции возникает, если для объявления переопределения не применяются все указанные ниже действия.
- Переопределенный базовый метод можно найти, как описано выше.
- Существует именно один такой переопределенный базовый метод. Это ограничение действует, только если тип базового класса является созданным типом, где подстановка аргументов типа делает подпись двух методов одинаковой.
- Переопределенный базовый метод — это виртуальный, абстрактный или переопределенный метод. Другими словами, переопределенный базовый метод не может быть статическим или не виртуальным.
- Переопределенный базовый метод не является запечатанным методом.
- Существует преобразование удостоверений между возвращаемым типом переопределенного базового метода и методом переопределения.
- Объявление переопределения и переопределенный базовый метод имеют те же объявленные специальные возможности. Другими словами, объявление переопределения не может изменить доступность виртуального метода. Тем не менее, если переопределенный базовый метод защищен внутренним и он объявлен в другой сборке, отличной от сборки, содержащей объявление переопределения, то объявленное объявление переопределения должно быть защищено.
- Объявление переопределения не указывает type_parameter_constraints_clause s. Вместо этого ограничения наследуются от переопределенного базового метода. Ограничения, которые являются параметрами типа в переопределенном методе, могут быть заменены аргументами типа в наследуемом ограничении. Это может привести к ограничениям, которые недопустимы при явном указании, таких как типы значений или запечатанные типы.
Пример. Ниже показано, как работают переопределяющие правила для универсальных классов:
abstract class C<T> { public virtual T F() {...} public virtual C<T> G() {...} public virtual void H(C<T> x) {...} } class D : C<string> { public override string F() {...} // Ok public override C<string> G() {...} // Ok public override void H(C<T> x) {...} // Error, should be C<string> } class E<T,U> : C<U> { public override U F() {...} // Ok public override C<U> G() {...} // Ok public override void H(C<T> x) {...} // Error, should be C<U> }
пример конца
Объявление переопределения может получить доступ к переопределенным базовым методу с помощью base_access (§12.8.15).
Пример. В следующем коде
class A { int x; public virtual void PrintFields() => Console.WriteLine($"x = {x}"); } class B : A { int y; public override void PrintFields() { base.PrintFields(); Console.WriteLine($"y = {y}"); } }
base.PrintFields()
вызов вызываетB
метод PrintFields, объявленный вA
. Base_access отключает механизм виртуального вызова и просто обрабатывает базовый метод как метод, отличныйvirtual
от метода. Если вызов был записанB
, он будет рекурсивно вызывать((A)this).PrintFields()
метод, объявленный в , а не тот, который объявлен в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
и предупреждение сообщается,A
так как объявление не включает новый модификатор.пример конца
Пример. В следующем коде
class A { public virtual void F() {} } class B : A { private new void F() {} // Hides A.F within body of B } class C : B { public override void F() {} // Ok, overrides A.F }
F
Метод вB
скрытии виртуальногоF
метода, унаследованного отA
. Так как новыйF
вB
закрытом доступе имеет частный доступ, его область включает только телоB
класса и не распространяется наC
. Поэтому объявлениеF
вC
разрешено переопределитьF
унаследованное отA
.пример конца
15.6.6 Запечатанные методы
Если объявление метода экземпляра включает sealed
модификатор, этот метод считается запечатанным методом. Запечатанный метод переопределяет унаследованный виртуальный метод с той же сигнатурой. Запечатанный метод также должен быть помечен модификатором override
.
sealed
Использование модификатора предотвращает дальнейшее переопределение метода производным классом.
Пример: пример
class A { public virtual void F() => Console.WriteLine("A.F"); public virtual void G() => Console.WriteLine("A.G"); } class B : A { public sealed override void F() => Console.WriteLine("B.F"); public override void G() => Console.WriteLine("B.G"); } class C : B { public override void G() => Console.WriteLine("C.G"); }
Класс
B
предоставляет два метода переопределения:F
метод с модификаторомsealed
и методомG
, который не имеет.B
Использование модификатораsealed
предотвращаетC
дальнейшее переопределениеF
.пример конца
15.6.7 Абстрактные методы
Если объявление метода экземпляра включает abstract
модификатор, этот метод считается абстрактным методом. Хотя абстрактный метод неявно также является виртуальным методом, он не может иметь модификатор virtual
.
Объявление абстрактного метода вводит новый виртуальный метод, но не предоставляет реализацию этого метода. Вместо этого не абстрактные производные классы необходимы для предоставления собственной реализации путем переопределения этого метода. Поскольку абстрактный метод не предоставляет фактической реализации, текст метода абстрактного метода просто состоит из точки с запятой.
Объявления абстрактных методов разрешены только в абстрактных классах (§15.2.2.2).
Пример. В следующем коде
public abstract class Shape { public abstract void Paint(Graphics g, Rectangle r); } public class Ellipse : Shape { public override void Paint(Graphics g, Rectangle r) => g.DrawEllipse(r); } public class Box : Shape { public override void Paint(Graphics g, Rectangle r) => g.DrawRect(r); }
Shape
Класс определяет абстрактное понятие объекта геометрической фигуры, который может закрасить себя. МетодPaint
абстрактен, так как нет понятной реализации по умолчанию.Ellipse
ИBox
классы являются конкретнымиShape
реализациями. Так как эти классы являются не абстрактными, они необходимы для переопределенияPaint
метода и предоставления фактической реализации.пример конца
Это ошибка времени компиляции для base_access (§12.8.15) для ссылки на абстрактный метод.
Пример. В следующем коде
abstract class A { public abstract void F(); } class B : A { // Error, base.F is abstract public override void F() => base.F(); }
Сообщается об ошибке во время компиляции для
base.F()
вызова, так как она ссылается на абстрактный метод.пример конца
Объявление абстрактного метода разрешено переопределить виртуальный метод. Это позволяет абстрактным классам принудительно повторно реализовать метод в производных классах и делает исходную реализацию метода недоступной.
Пример. В следующем коде
class A { public virtual void F() => Console.WriteLine("A.F"); } abstract class B: A { public abstract override void F(); } class C : B { public override void F() => Console.WriteLine("C.F"); }
класс
A
объявляет виртуальный метод, классB
переопределяет этот метод абстрактным методом, а классC
переопределяет абстрактный метод для предоставления собственной реализации.пример конца
Внешние методы 15.6.8
Если объявление метода включает extern
модификатор, метод считается внешним методом. Внешние методы реализуются внешне, как правило, с помощью языка, отличного от C#. Поскольку объявление внешнего метода не обеспечивает фактической реализации, текст метода внешнего метода просто состоит из запятой. Внешний метод не должен быть универсальным.
Механизм, с помощью которого достигается связывание с внешним методом, определяется реализацией.
Пример. В следующем примере показано использование
extern
модификатора и атрибутаDllImport
:class Path { [DllImport("kernel32", SetLastError=true)] static extern bool CreateDirectory(string name, SecurityAttribute sa); [DllImport("kernel32", SetLastError=true)] static extern bool RemoveDirectory(string name); [DllImport("kernel32", SetLastError=true)] static extern int GetCurrentDirectory(int bufSize, StringBuilder buf); [DllImport("kernel32", SetLastError=true)] static extern bool SetCurrentDirectory(string name); }
пример конца
Частичные методы 15.6.9
Если объявление метода включает partial
модификатор, этот метод, как говорят, является частичным методом. Частичные методы могут быть объявлены только как члены частичных типов (§15.2.7) и имеют ряд ограничений.
Частичные методы могут быть определены в одной части объявления типа и реализованы в другой. Реализация является необязательной; Если часть не реализует частичный метод, объявление частичного метода и все вызовы к нему удаляются из объявления типа, полученного из сочетания частей.
Частичные методы не определяют модификаторы доступа; они неявно закрыты. Их тип возвращаемого значения должен быть void
, и их параметры не должны быть выходными параметрами. Частичный идентификатор распознается как контекстное ключевое слово (§6.4.4) в объявлении метода, только если оно отображается непосредственно перед ключевым словом void
. Частичный метод не может явно реализовать методы интерфейса.
Существует два типа объявлений частичных методов: если текст объявления метода является точкой с запятой, объявление считается определяющим частичным объявлением метода. Если тело отличается от точки с запятой, объявление считается реализацией частичного объявления метода. В частях объявления типа может быть только одно определение частичного объявления метода с заданной сигнатурой, и может быть только одно реализующее частичное объявление метода с заданной сигнатурой. Если задано реализующее объявление частичного метода, должно существовать соответствующее определение частичного объявления метода, и объявления должны совпадать, как указано в следующем:
- Объявления должны иметь одинаковые модификаторы (хотя и не обязательно в том же порядке), имя метода, количество параметров типа и количество параметров.
- Соответствующие параметры в объявлениях должны иметь одинаковые модификаторы (хотя и не обязательно в том же порядке) и те же типы, или типы, преобразуемые удостоверения (различия модуля в именах параметров типа).
- Соответствующие параметры типа в объявлениях должны иметь одинаковые ограничения (различия в модуле в именах параметров типа).
Реализующее объявление частичного метода может отображаться в той же части, что и соответствующее определение объявления частичного метода.
Только определяющий частичный метод участвует в разрешении перегрузки. Таким образом, задано ли объявление реализации, выражения вызова могут разрешаться для вызовов частичного метода. Так как частичный метод всегда возвращается void
, такие выражения вызова всегда будут операторами выражений. Кроме того, поскольку частичный метод неявно private
, такие инструкции всегда будут возникать в одной из частей объявления типа, в которых объявлен частичный метод.
Примечание. Определение сопоставления определений и реализации объявлений частичных методов не требует сопоставления имен параметров. Это может привести к удивительному, хотя и хорошо определенному, поведению при использовании именованных аргументов (§12.6.2.1). Например, при определении объявления частичного метода для
M
одного файла и реализации объявления частичного метода в другом файле:// File P1.cs: partial class P { static partial void M(int x); } // File P2.cs: partial class P { static void Caller() => M(y: 0); static partial void M(int y) {} }
является недопустимым , так как вызов использует имя аргумента из реализации, а не определение объявления частичного метода.
конечная заметка
Если часть объявления частичного типа не содержит реализующего объявления для данного частичного метода, любая инструкция выражения, вызывающая ее, просто удаляется из объявления объединенного типа. Таким образом, выражение вызова, включая любые вложенные выражения, не влияет на время выполнения. Сам частичный метод также удаляется и не будет членом объявления объединенного типа.
Если для заданного частичного метода существует объявление реализации, вызовы частичных методов сохраняются. Частичный метод приводит к объявлению метода, аналогичному объявлению реализации частичного метода, за исключением следующего:
Модификатор
partial
не включен.Атрибуты в итоговом объявлении метода — это объединенные атрибуты определения и реализации объявления частичного метода в неопределенном порядке. Дубликаты не удаляются.
Атрибуты параметров итогового объявления метода — это объединенные атрибуты соответствующих параметров определения и реализации объявления частичного метода в неопределенном порядке. Дубликаты не удаляются.
Если для частичного метода M
задано определение объявления, но не реализующее объявление, применяются следующие ограничения:
Это ошибка времени компиляции для создания делегата из
M
(§12.8.17.6).Это ошибка во время компиляции, которая ссылается
M
на анонимную функцию, преобразованную в тип дерева выражений (§8.6).Выражения, происходящие в рамках вызова
M
, не влияют на определенное состояние назначения (§9.4), что может привести к ошибкам во время компиляции.M
не может быть точкой входа для приложения (§7.1).
Частичные методы полезны для разрешения одной части объявления типа для настройки поведения другой части, например той, которая создается средством. Рассмотрим следующее объявление частичного класса:
partial class Customer
{
string name;
public string Name
{
get => name;
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
partial void OnNameChanging(string newName);
partial void OnNameChanged();
}
Если этот класс компилируется без каких-либо других частей, определение объявлений частичных методов и их вызовов будет удалено, а итоговое объявление объединенного класса будет эквивалентно следующим:
class Customer
{
string name;
public string Name
{
get => name;
set => name = value;
}
}
Предположим, что предоставляется другая часть, которая предоставляет объявления частичных методов:
partial class Customer
{
partial void OnNameChanging(string newName) =>
Console.WriteLine($"Changing {name} to {newName}");
partial void OnNameChanged() =>
Console.WriteLine($"Changed to {name}");
}
Затем итоговое объявление объединенного класса будет эквивалентно следующему:
class Customer
{
string name;
public string Name
{
get => name;
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
void OnNameChanging(string newName) =>
Console.WriteLine($"Changing {name} to {newName}");
void OnNameChanged() =>
Console.WriteLine($"Changed to {name}");
}
Методы расширения 15.6.10
Если первый параметр метода включает this
модификатор, этот метод считается методом расширения. Методы расширения объявляются только в не универсальных не вложенных статических классах. Первый параметр метода расширения ограничен следующим образом:
- Он может быть только входным параметром, если он имеет тип значения
- Это может быть только ссылочный параметр, если он имеет тип значения или имеет универсальный тип, ограниченный структурой
- Он не должен быть типом указателя.
Пример. Ниже приведен пример статического класса, объявляющего два метода расширения:
public static class Extensions { public static int ToInt32(this string s) => Int32.Parse(s); public static T[] Slice<T>(this T[] source, int index, int count) { if (index < 0 || count < 0 || source.Length - index < count) { throw new ArgumentException(); } T[] result = new T[count]; Array.Copy(source, index, result, 0, count); return result; } }
пример конца
Метод расширения — это обычный статический метод. Кроме того, если его вложенный статический класс находится в области, метод расширения может вызываться с помощью синтаксиса вызова метода экземпляра (§12.8.10.3), используя выражение приемника в качестве первого аргумента.
Пример. В следующей программе используются методы расширения, объявленные выше:
static class Program { static void Main() { string[] strings = { "1", "22", "333", "4444" }; foreach (string s in strings.Slice(1, 2)) { Console.WriteLine(s.ToInt32()); } } }
Метод
Slice
доступен дляstring[]
метода , иToInt32
метод доступен вstring
, так как они были объявлены как методы расширения. Значение программы совпадает со следующим значением, используя обычные статические вызовы метода:static class Program { static void Main() { string[] strings = { "1", "22", "333", "4444" }; foreach (string s in Extensions.Slice(strings, 1, 2)) { Console.WriteLine(Extensions.ToInt32(s)); } } }
пример конца
Текст метода 15.6.11
Текст метода объявления метода состоит из блока, тела выражения или точки с запятой.
Абстрактные и внешние объявления методов не предоставляют реализацию метода, поэтому их тела методов просто состоят из точки с запятой. Для любого другого метода текст метода является блоком (§13.3), который содержит инструкции, выполняемые при вызове этого метода.
Действующий тип возвращаемого метода заключается void
в том, имеет ли тип возвращаемого значения void
или если метод является асинхронным, а тип возвращаемого значения — «TaskType»
(§15.15.1). В противном случае эффективный тип возвращаемого метода, не асинхронного, является его типом возвращаемого значения, а эффективный тип возвращаемого метода с типом возвращаемого значения «TaskType»<T>
(§15.15.1).T
Если действующий тип возвращаемого метода void
и метод имеет блоковое тело, return
операторы (§13.10.5) в блоке не должны указывать выражение. Если выполнение блока метода void завершается нормально (т. е. потоки управления от конца текста метода), этот метод просто возвращается вызывающему объекту.
Если эффективный тип возвращаемого метода void
и метод имеет тело выражения, выражение E
должно быть statement_expression, а тело точно эквивалентно блоку формы { E; }
.
Для метода return-by-value (§15.6.1) каждая инструкция return в тексте этого метода должна указывать выражение, которое неявно преобразуется в действующий тип возвращаемого значения.
Для метода return-by-ref (§15.6.1) каждая инструкция return в тексте этого метода должна указывать выражение, тип которого относится к эффективному типу возвращаемого типа, и имеет ссылочный безопасный контекст вызывающего контекста (§9.7.2).
Для методов возвращаемых по значению и возвращаемых по ссылкам конечная точка тела метода не должна быть доступной. Другими словами, элемент управления не может передаваться из конца текста метода.
Пример. В следующем коде
class A { public int F() {} // Error, return value required public int G() { return 1; } public int H(bool b) { if (b) { return 1; } else { return 0; } } public int I(bool b) => b ? 1 : 0; }
Метод, возвращающий
F
значение, приводит к ошибке во время компиляции, так как элемент управления может вытекать из конца текста метода.G
МетодыH
правильны, так как все возможные пути выполнения заканчиваются в операторе return, указывающего возвращаемое значение. МетодI
правильный, так как его тело эквивалентно блоку с одной инструкцией возврата в нем.пример конца
Свойства 15.7
15.7.1 Общие
Свойство — это элемент, предоставляющий доступ к характеристике объекта или класса. Примеры свойств включают длину строки, размер шрифта, подпись окна и имя клиента. Свойства — это естественное расширение полей, как именованные элементы с связанными типами, так и синтаксис для доступа к полям и свойствам одинаковы. Однако свойства, в отличие от полей, не указывают места хранения. Вместо этого свойства содержат методы доступа, в которых описаны инструкции для выполнения при чтении или записи значений. Таким образом, свойства предоставляют механизм связывания действий с чтением и записью характеристик объекта или класса; кроме того, они позволяют вычислить такие характеристики.
Свойства объявляются с помощью property_declaration:
property_declaration
: attributes? property_modifier* type member_name property_body
| attributes? property_modifier* ref_kind type member_name ref_property_body
;
property_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
property_body
: '{' accessor_declarations '}' property_initializer?
| '=>' expression ';'
;
property_initializer
: '=' variable_initializer ';'
;
ref_property_body
: '{' ref_get_accessor_declaration '}'
| '=>' 'ref' variable_reference ';'
;
unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).
Существует два типа property_declaration:
- Первый объявляет свойство, отличное от ссылок. Его значение имеет тип типа. Это свойство может быть доступно для чтения и /или записи.
- Второй объявляет свойство ref-valued. Его значением является variable_reference (§9.5), которые могут быть
readonly
переменными типа. Это свойство доступно только для чтения.
Property_declaration может включать набор атрибутов (§22) и любой из разрешенных видов объявленной доступности (§15.3.6), (new
), (§15.7.2static
§15.6.4, virtual
§15.7.6), (§15.6.5, override
), (§15.6.6abstract
, §15.7.6) и (extern
) модификаторов.
Объявления свойств применяются к тем же правилам, что и объявления методов (§15.6) в отношении допустимых сочетаний модификаторов.
Member_name (§15.6.1) указывает имя свойства. Если свойство не является явной реализацией элемента интерфейса, member_name просто является идентификатором. Для явной реализации элемента интерфейса (§18.6.2) member_name состоит из interface_type , за которым следует ".
" и идентификатор.
Тип свойства должен быть по крайней мере доступным как сам свойство (§7.5.5).
Property_body может состоять из текста инструкции или текста выражения. В тексте инструкции accessor_declarations, которые должны быть заключены в маркеры "{
" и "}
", объявляют методы доступа (§15.7.3) свойства. Методы доступа указывают исполняемые инструкции, связанные с чтением и записью свойства.
В property_body тексте выражения, состоящем из =>
выражения, за которым следует выражениеE
, и точка с запятой точно эквивалентна тексту { get { return E; } }
инструкции, поэтому можно использовать только для указания свойств только для чтения, где результат метода доступа получается одним выражением.
Property_initializer может быть задано только для автоматически реализованного свойства (§15.7.4) и вызывает инициализацию базового поля таких свойств со значением, заданным выражением.
Ref_property_body может состоять из текста инструкции или текста выражения. В тексте инструкции get_accessor_declaration объявляет метод доступа (§15.7.3) свойства. Метод доступа задает исполняемые инструкции, связанные с чтением свойства.
В ref_property_body текст выражения, состоящий из =>
последующихref
, V
и точка с запятой точно эквивалентны тексту { get { return ref V; } }
инструкции.
Примечание. Несмотря на то, что синтаксис для доступа к свойству аналогичен значению поля, свойство не классифицируется как переменная. Таким образом, невозможно передать свойство в качестве аргумента
in
илиout
аргумента, если свойство не имеет ссылочного значения и поэтому возвращает ссылку на переменную (ref
). конечная заметка
Если объявление свойства включает extern
модификатор, свойство, как считается, является внешним свойством. Поскольку объявление внешнего свойства не предоставляет фактической реализации, каждая из accessor_bodyв его accessor_declarations должна быть точкой с запятой.
Свойства статического и экземпляра 15.7.2
Если объявление свойства включает static
модификатор, свойство, как утверждается, является статическим свойством. Если модификатор отсутствуетstatic
, свойство считается свойством экземпляра.
Статичное свойство не связано с определенным экземпляром, и это ошибка во время компиляции, ссылающейся this
на методы доступа статического свойства.
Свойство экземпляра связано с заданным экземпляром класса, и к данному экземпляру можно получить доступ как this
(§12.8.14) в методах доступа этого свойства.
Различия между статическими и экземплярами рассматриваются далее в §15.3.8.
15.7.3 Методы доступа
Примечание. Это предложение относится к обоим свойствам (§15.7) и индексаторам (§15.9). Предложение записывается с точки зрения свойств, при чтении для индексаторов заменяющих индексатор/индексаторов свойств и свойств и ознакомьтесь со списком различий между свойствами и индексаторами, указанными в §15.9.2. конечная заметка
Accessor_declarations свойства указывают исполняемые инструкции, связанные с записью и/или чтением этого свойства.
accessor_declarations
: get_accessor_declaration set_accessor_declaration?
| set_accessor_declaration get_accessor_declaration?
;
get_accessor_declaration
: attributes? accessor_modifier? 'get' accessor_body
;
set_accessor_declaration
: attributes? accessor_modifier? 'set' accessor_body
;
accessor_modifier
: 'protected'
| 'internal'
| 'private'
| 'protected' 'internal'
| 'internal' 'protected'
| 'protected' 'private'
| 'private' 'protected'
;
accessor_body
: block
| '=>' expression ';'
| ';'
;
ref_get_accessor_declaration
: attributes? accessor_modifier? 'get' ref_accessor_body
;
ref_accessor_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
Accessor_declarations состоят из get_accessor_declaration, set_accessor_declaration или обоих. Каждое объявление доступа состоит из необязательных атрибутов, необязательных accessor_modifier, маркера get
или set
accessor_body.
Для свойства ref-value, ref_get_accessor_declaration состоит из необязательных атрибутов, необязательных accessor_modifier маркера get
, а затем ref_accessor_body.
Использование accessor_modifiers регулируется следующими ограничениями:
- Accessor_modifier не следует использовать в интерфейсе или в явной реализации элемента интерфейса.
- Для свойства или индексатора, не имеющего
override
модификатора, accessor_modifier разрешено только в том случае, если свойство или индексатор имеет метод доступа get и set, а затем разрешено только для одного из этих методов доступа. - Для свойства или индексатора, включающего
override
модификатор, метод доступа должен соответствовать accessor_modifier, если таковой имеется, переопределенного метода доступа. - Accessor_modifier должен объявить специальные возможности, строго ограничивающие, чем объявленные специальные возможности самого свойства или индексатора. Чтобы быть точным:
- Если свойство или индексатор имеет объявленную доступность, то специальные возможности
public
, объявленные accessor_modifier, могут иметь значениеprivate protected
, ,protected internal
internal
protected
или .private
- Если свойство или индексатор имеет объявленную доступность, то специальные возможности
protected internal
, объявленные accessor_modifier, могут иметь значениеprivate protected
, ,protected private
internal
protected
или .private
- Если свойство или индексатор имеет объявленную доступность или , то специальные возможности
internal
, объявленныеprotected
, должны быть либоprivate protected
.private
- Если свойство или индексатор имеет объявленную доступность, то специальные возможности
private protected
, объявленные accessor_modifier , должны бытьprivate
. - Если свойство или индексатор имеет объявленную доступность
private
, accessor_modifier не может использоваться.
- Если свойство или индексатор имеет объявленную доступность, то специальные возможности
Для abstract
свойств, extern
отличных от ссылок, любые accessor_body для каждого указанного метода доступа — это просто точка с запятой. Не абстрактное, не экстерновое свойство, но не индексатор, может также иметь accessor_body для всех методов доступа, указанных как точка с запятой, в этом случае это автоматически реализованное свойство (§15.7.4). Автоматически реализованное свойство должно иметь по крайней мере метод доступа. Для методов доступа любого другого не абстрактного, неисключаемого свойства accessor_body:
- блок, указывающий инструкции, выполняемые при вызове соответствующего метода доступа; или
- текст выражения, состоящий из
=>
выражения и с запятой, и обозначает одно выражение, выполняемое при вызове соответствующего метода доступа.
Для abstract
свойств, extern
возвращаемых значением, ref_accessor_body — это просто точка с запятой. Для метода доступа любого другого не абстрактного, неисключаемого свойства ref_accessor_body:
- блок, указывающий операторы, выполняемые при вызове метода доступа; или
- текст выражения, состоящий из последующих
=>
ref
, variable_reference и точки с запятой. Ссылка на переменную вычисляется при вызове метода доступа.
Метод доступа для свойства, отличного от ссылок, соответствует методу без параметров с возвращаемым значением типа свойства. За исключением целевого объекта назначения, если такое свойство ссылается в выражении, его метод доступа вызывается для вычисления значения свойства (§12.2.2).
Текст метода доступа, отличного от ссылок, должен соответствовать правилам методов возврата значений, описанным в разделе 15.6.11. В частности, все return
операторы в тексте метода доступа get должны указывать выражение, которое неявно преобразуется в тип свойства. Кроме того, конечная точка метода доступа не может быть достигнута.
Метод доступа для свойства ref-value соответствует методу без параметров с возвращаемым значением variable_reference переменной типа свойства. Если такое свойство ссылается в выражении, его метод доступа вызывается для вычисления значения variable_reference свойства. Эта ссылка на переменную, как и любая другая, затем используется для чтения или для нечитаемых variable_references, записывайте указанную переменную в соответствии с контекстом.
Пример. В следующем примере показано свойство ref-valued в качестве целевого объекта назначения:
class Program { static int field; static ref int Property => ref field; static void Main() { field = 10; Console.WriteLine(Property); // Prints 10 Property = 20; // This invokes the get accessor, then assigns // via the resulting variable reference Console.WriteLine(field); // Prints 20 } }
пример конца
Текст метода доступа get для свойства ref-valued должен соответствовать правилам для методов, возвращаемых ref-valued, описанных в разделе 15.6.11.
Метод доступа набора соответствует методу с одним параметром значения типа свойства и возвращаемого void
типа. Неявный параметр набора доступа всегда называется value
. Если свойство ссылается в качестве целевого объекта назначения (§12.21), либо как операнды или ++
(–-
§12.8.16, §12.9.6), метод доступа к набору вызывается с аргументом, предоставляющим новое значение (§12.21.21.2). Текст набора доступа должен соответствовать правилам void
методов, описанным в разделе 15.6.11. В частности, операторы возврата в тексте набора доступа не допускаются для указания выражения. Так как метод доступа набора неявно имеет параметр с именем value
, это ошибка во время компиляции для локальной переменной или объявления констант в наборе доступа, чтобы иметь это имя.
В зависимости от наличия или отсутствия методов доступа get и set свойство классифицируется следующим образом:
- Свойство, которое включает как метод доступа, так и набор доступа, как говорят, является свойством чтения и записи.
- Свойство, которое имеет только метод доступа для чтения, как говорят, является свойством только для чтения. Это ошибка во время компиляции для свойства только для чтения, которое должно быть целевым объектом назначения.
- Свойство, которое имеет только набор доступа, как говорят, является свойством только для записи. За исключением целевого объекта назначения, это ошибка во время компиляции для ссылки на свойство только для записи в выражении.
Примечание. Пре- и постфикс
++
--
и операторы и составные операторы присваивания не могут применяться к свойствам только для записи, так как эти операторы считывают старое значение операнда перед записью нового. конечная заметка
Пример. В следующем коде
public class Button : Control { private string caption; public string Caption { get => caption; set { if (caption != value) { caption = value; Repaint(); } } } public override void Paint(Graphics g, Rectangle r) { // Painting code goes here } }
Элемент
Button
управления объявляет общедоступноеCaption
свойство. Метод доступа свойства Caption возвращаетstring
хранящийся в частномcaption
поле. Метод доступа набора проверяет, отличается ли новое значение от текущего значения, а если да, он сохраняет новое значение и переопределяет элемент управления. Свойства часто следуют приведенному выше шаблону: метод доступа просто возвращает значение, хранящееся вprivate
поле, и метод доступа задает этоprivate
поле, а затем выполняет дополнительные действия, необходимые для полного обновления состояния объекта. Учитывая приведенныйButton
выше класс, ниже приведен пример использованияCaption
свойства:Button okButton = new Button(); okButton.Caption = "OK"; // Invokes set accessor string s = okButton.Caption; // Invokes get accessor
Здесь метод доступа set вызывается путем назначения значения свойству, а метод доступа вызывается путем ссылки на свойство в выражении.
пример конца
Методы доступа к свойству get и set не являются отдельными элементами, и нельзя объявлять методы доступа свойства отдельно.
Пример: пример
class A { private string name; // Error, duplicate member name public string Name { get => name; } // Error, duplicate member name public string Name { set => name = value; } }
не объявляет одно свойство чтения и записи. Скорее, он объявляет два свойства с одинаковым именем, одним только для чтения и одним только записью. Так как два члена, объявленные в одном классе, не могут иметь одинаковое имя, в примере возникает ошибка во время компиляции.
пример конца
Когда производный класс объявляет свойство по тому же имени, что и унаследованное свойство, производное свойство скрывает унаследованное свойство в отношении чтения и записи.
Пример. В следующем коде
class A { public int P { set {...} } } class B : A { public new int P { get {...} } }
P
Свойство в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
для записи вA
. Обратите внимание, что приведение можно использовать для доступа к скрытомуP
свойству.пример конца
В отличие от открытых полей, свойства обеспечивают разделение внутреннего состояния объекта и его общедоступного интерфейса.
Пример. Рассмотрим следующий код, который использует структуру
Point
для представления расположения:class Label { private int x, y; private string caption; public Label(int x, int y, string caption) { this.x = x; this.y = y; this.caption = caption; } public int X => x; public int Y => y; public Point Location => new Point(x, y); public string Caption => caption; }
Label
Здесь класс использует дваint
поляx
иy
сохраняет его расположение. Расположение предоставляется как вX
виде, так иY
в свойстве, а также в качествеLocation
свойства типаPoint
. Если в будущей версииLabel
становится удобнее хранить расположениеPoint
как внутренне, изменение может быть сделано без влияния на общедоступный интерфейс класса:class Label { private Point location; private string caption; public Label(int x, int y, string caption) { this.location = new Point(x, y); this.caption = caption; } public int X => location.X; public int Y => location.Y; public Point Location => location; public string Caption => caption; }
Если
x
бы иy
вместо этого былиpublic readonly
поля, это было бы невозможно сделать такое изменение классаLabel
.пример конца
Примечание. Предоставление состояния через свойства не обязательно является менее эффективным, чем предоставление полей напрямую. В частности, если свойство не является виртуальным и содержит только небольшой объем кода, среда выполнения может заменить вызовы методов доступа фактическим кодом методов доступа. Этот процесс называется встраиванием, и он делает доступ к свойствам максимально эффективным, как доступ к полям, но сохраняет повышенную гибкость свойств. конечная заметка
Пример. Так как вызов метода доступа get концептуально эквивалентен чтению значения поля, он считается плохим стилем программирования для получения методов доступа к наблюдаемым побочным эффектам. В примере
class Counter { private int next; public int Next => next++; }
Значение
Next
свойства зависит от количества ранее доступ к свойству. Таким образом, доступ к свойству создает наблюдаемый побочный эффект, а свойство должно быть реализовано как метод.Соглашение "без побочных эффектов" для методов доступа не означает, что методы доступа всегда должны быть записаны просто для возврата значений, хранящихся в полях. Действительно, методы доступа часто вычисляют значение свойства, получая доступ к нескольким полям или вызывая методы. Однако правильно разработанный метод доступа не выполняет никаких действий, которые вызывают наблюдаемые изменения в состоянии объекта.
пример конца
Свойства можно использовать для задержки инициализации ресурса до момента его первой ссылки.
Пример:
public class Console { private static TextReader reader; private static TextWriter writer; private static TextWriter error; public static TextReader In { get { if (reader == null) { reader = new StreamReader(Console.OpenStandardInput()); } return reader; } } public static TextWriter Out { get { if (writer == null) { writer = new StreamWriter(Console.OpenStandardOutput()); } return writer; } } public static TextWriter Error { get { if (error == null) { error = new StreamWriter(Console.OpenStandardError()); } return error; } } ... }
Класс
Console
содержит три свойства,In
Out
иError
, которые представляют стандартные входные, выходные и ошибки устройства соответственно. Предоставляя эти элементы в качестве свойств, класс может отложить их инициализацию до тех пор,Console
пока они не будут использованы. Например, при первом ссылкеOut
на свойство, как вConsole.Out.WriteLine("hello, world");
Создается базовый
TextWriter
элемент для выходного устройства. Однако если приложение не ссылается наIn
свойства иError
свойства, то для этих устройств не создаются объекты.пример конца
15.7.4 Автоматически реализованные свойства
Автоматически реализованное свойство (или автоматическое свойство для короткого) — это не абстрактное, неисправное, неотражаемое свойство с точкой с запятой accessor_body. Автоматические свойства должны иметь метод доступа к методу доступа и при необходимости может иметь метод доступа к набору.
Если свойство указывается как автоматически реализованное свойство, скрытое поле резервного копирования автоматически доступно для свойства, а методы доступа реализуются для чтения и записи в это резервное поле. Скрытое резервное поле недоступно, оно может быть прочитано и записано только с помощью автоматически реализованных методов доступа к свойствам, даже в пределах содержащего типа. Если свойство auto-property не имеет метода доступа, то поле резервного копирования считается readonly
(§15.5.3).
readonly
Как и поле, автоматическое свойство только для чтения также может быть назначено в тексте конструктора включающем классе. Такое назначение назначается непосредственно в поле резервной копии только для чтения свойства.
При необходимости для автоматического свойства может быть property_initializer, который применяется непосредственно к резервному полю в качестве variable_initializer (§17.7).
Пример:
public class Point { public int X { get; set; } // Automatically implemented public int Y { get; set; } // Automatically implemented }
эквивалентен следующему объявлению:
public class Point { private int x; private int y; public int X { get { return x; } set { x = value; } } public int Y { get { return y; } set { y = value; } } }
пример конца
Пример. В следующем примере
public class ReadOnlyPoint { public int X { get; } public int Y { get; } public ReadOnlyPoint(int x, int y) { X = x; Y = y; } }
эквивалентен следующему объявлению:
public class ReadOnlyPoint { private readonly int __x; private readonly int __y; public int X { get { return __x; } } public int Y { get { return __y; } } public ReadOnlyPoint(int x, int y) { __x = x; __y = y; } }
Назначения в поле только для чтения допустимы, так как они происходят в конструкторе.
пример конца
Хотя резервное поле скрыто, это поле может иметь атрибуты, предназначенные для полей, применяются непосредственно к нему с помощью автоматически реализованных property_declaration свойства (§15.7.1).
Пример: следующий код
[Serializable] public class Foo { [field: NonSerialized] public string MySecret { get; set; } }
Приводит к применению целевого поля атрибута
NonSerialized
к созданному компилятором полю резервного копирования, как если бы код был написан следующим образом:[Serializable] public class Foo { [NonSerialized] private string _mySecretBackingField; public string MySecret { get { return _mySecretBackingField; } set { _mySecretBackingField = value; } } }
пример конца
Специальные возможности 15.7.5
Если метод доступа имеет accessor_modifier, домен специальных возможностей (§7.5.3) метода доступа определяется с помощью объявленной доступности accessor_modifier. Если у метода доступа нет accessor_modifier, домен специальных возможностей метода доступа определяется из объявленной доступности свойства или индексатора.
Наличие accessor_modifier никогда не влияет на поиск элементов (§12.5) или разрешение перегрузки (§12.6.4). Модификаторы свойства или индексатора всегда определяют, к каким свойствам или индексатору привязаны независимо от контекста доступа.
После выбора определенного свойства, отличного от ref-value или неref-valued indexer, домены специальных возможностей определенных средств доступа используются для определения допустимости использования:
- Если использование является значением (§12.2.2), метод доступа должен существовать и быть доступным.
- Если использование является целевым объектом простого назначения (§12.21.2), метод доступа к набору должен существовать и быть доступным.
- Если использование является целевым объектом составного назначения (§12.21.4
--
§12.8.16, §12.9.6), как методы доступа, так и набор доступа должны существовать и быть доступными.
Пример. В следующем примере свойство скрыто свойством
A.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-valued или ref-valued indexer; указывает, является ли использование значением, целевым объектом простого назначения или целевым объектом составного назначения; Домен специальных возможностей задействованного метода доступа используется для определения допустимости использования.
Метод доступа, используемый для реализации интерфейса, не должен иметь accessor_modifier. Если для реализации интерфейса используется только один метод доступа, другой метод доступа может быть объявлен с помощью accessor_modifier:
Пример:
public interface I { string Prop { get; } } public class C : I { public string Prop { get => "April"; // Must not have a modifier here internal set {...} // Ok, because I.Prop has no set accessor } }
пример конца
15.7.6 Виртуальные, запечатанные, переопределения и абстрактные методы доступа
Примечание. Это предложение относится к обоим свойствам (§15.7) и индексаторам (§15.9). Предложение записывается с точки зрения свойств, при чтении для индексаторов заменяющих индексатор/индексаторов свойств и свойств и ознакомьтесь со списком различий между свойствами и индексаторами, указанными в §15.9.2. конечная заметка
Объявление виртуального свойства указывает, что методы доступа свойства являются виртуальными. Модификатор virtual
применяется ко всем не закрытым доступам свойства. Если метод доступа виртуального свойства имеет private
accessor_modifier, частный метод доступа неявно не является виртуальным.
Объявление абстрактного свойства указывает, что методы доступа свойства являются виртуальными, но не предоставляют фактическую реализацию методов доступа. Вместо этого не абстрактные производные классы необходимы для предоставления собственной реализации для методов доступа путем переопределения свойства. Поскольку метод доступа для объявления абстрактного свойства не предоставляет фактической реализации, его accessor_body просто состоит из точки с запятой. Абстрактное свойство не должно иметь private
метод доступа.
Объявление свойства, включающее abstract
override
и модификаторы, указывает, что свойство абстрактно и переопределяет базовое свойство. Методы доступа такого свойства также абстрактны.
Объявления абстрактных свойств разрешены только в абстрактных классах (§15.2.2.2). Методы доступа унаследованного виртуального свойства можно переопределить в производном классе, включив объявление свойства, указывающее директиву override
. Это называется объявлением переопределения свойств. Объявление переопределяющего свойства не объявляет новое свойство. Вместо этого он просто специализируется на реализации методов доступа существующего виртуального свойства.
Объявление переопределения и переопределенное базовое свойство должны иметь те же объявленные специальные возможности. Другими словами, объявление переопределения не должно изменять доступность базового свойства. Тем не менее, если переопределенное базовое свойство защищено внутренним и оно объявляется в другой сборке, отличной от сборки, содержащей объявление переопределения, то объявленное объявление переопределения должно быть защищено. Если унаследованное свойство имеет только один метод доступа (т. е. если унаследованное свойство доступно только для чтения или только для записи), то переопределяющее свойство должно включать только этот метод доступа. Если унаследованное свойство включает оба метода доступа (т. е. если унаследованное свойство является чтением и записью), то переопределяющее свойство может включать один метод доступа или оба метода доступа. Должно быть преобразование удостоверений между типом переопределения и унаследованным свойством.
Объявление свойства переопределения может включать sealed
модификатор. Использование этого модификатора предотвращает дальнейшее переопределение свойства производным классом. Также запечатаны методы доступа к запечатанном свойству.
За исключением различий в синтаксисе объявления и вызова, виртуальных, запечатанных, переопределениях и абстрактных методах доступа работают точно так же, как виртуальные, запечатанные, переопределения и абстрактные методы. В частности, правила, описанные в §15.6.4, §15.6.5, §15.6.6 и §15.6.7 применяются, как если бы методы доступа были методами соответствующей формы:
- Метод доступа get соответствует методу без параметров с возвращаемым значением типа свойства и теми же модификаторами, что и содержащее свойство.
- Метод доступа набора соответствует методу с одним параметром значения типа свойства, типом возвращаемого пустоты и теми же модификаторами, что и содержащее свойство.
Пример. В следующем коде
abstract class A { int y; public virtual int X { get => 0; } public virtual int Y { get => y; set => y = value; } public abstract int Z { get; set; } }
X
является свойством только для чтения,Y
является свойством виртуальной записи для чтения и записи иZ
является абстрактным свойством чтения и записи. ПосколькуZ
это абстрактно, содержащий класс A также должен быть объявлен абстрактным.Ниже показан класс, производный от
A
следующего:class B : A { int z; public override int X { get => base.X + 1; } public override int Y { set => base.Y = value < 0 ? 0: value; } public override int Z { get => z; set => z = value; } }
Здесь объявления и
X
Y
переопределяют объявленияZ
свойств. Каждое объявление свойства точно соответствует модификаторам специальных возможностей, типу и имени соответствующего унаследованного свойства. Метод доступаX
и метод доступа к набору для доступа к унаследованным доступам с помощью базового ключевогоY
слова. ОбъявлениеZ
переопределяет как абстрактные методы доступа, таким образом, в нем отсутствуют выдающиесяabstract
членыB
функции иB
разрешено быть не абстрактным классом.пример конца
Когда свойство объявляется как переопределение, все переопределенные методы доступа должны быть доступны для переопределяющего кода. Кроме того, объявленные специальные возможности как свойства, так и индексатора, и методы доступа должны соответствовать переопределенному члену и методу доступа.
Пример:
public class B { public virtual int P { get {...} protected set {...} } } public class D: B { public override int P { get {...} // Must not have a modifier here protected set {...} // Must specify protected here } }
пример конца
15.8 События
15.8.1 Общие
Событие — это член, который позволяет объекту или классу предоставлять уведомления. Клиенты могут присоединить исполняемый код для событий, предоставив обработчики событий.
События объявляются с помощью event_declarations:
event_declaration
: attributes? event_modifier* 'event' type variable_declarators ';'
| attributes? event_modifier* 'event' type member_name
'{' event_accessor_declarations '}'
;
event_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
event_accessor_declarations
: add_accessor_declaration remove_accessor_declaration
| remove_accessor_declaration add_accessor_declaration
;
add_accessor_declaration
: attributes? 'add' block
;
remove_accessor_declaration
: attributes? 'remove' block
;
unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).
Event_declaration может включать набор атрибутов (§22) и любой из разрешенных видов объявленной доступности (§15.3.6), (new
), (§15.6.3, static
), (§15.6.4, virtual
§15.8.5), (§15.6.5, override
), (§15.6.6abstract
, §15.8.5) и (extern
) модификаторов.
Объявления событий применяются к тем же правилам, что и объявления методов (§15.6) в отношении допустимых сочетаний модификаторов.
Тип объявления события должен быть delegate_type (§8.2.8), и что delegate_type должно быть по крайней мере так же доступно, как само событие (§7.5.5).
Объявление события может включать event_accessor_declarations. Однако если это не так, для неисстрактных событий компилятор должен предоставлять их автоматически (§15.8.2); для событий extern
методы доступа предоставляются внешним образом.
Объявление события, в котором event_accessor_declaration event_accessor_declaration определяется одно или несколько событий — по одному для каждого из variable_declarator. Атрибуты и модификаторы применяются ко всем элементам, объявленным таким event_declaration.
Это ошибка во время компиляции для event_declaration для включения модификатора и abstract
.
Если объявление события включает extern
модификатор, событие считается внешним событием. Так как объявление внешнего события не обеспечивает фактической реализации, это ошибка для включения extern
модификатора и event_accessor_declarations.
Это ошибка во время компиляции для variable_declarator объявления события с abstract
модификатором или external
модификатором для включения variable_initializer.
Событие можно использовать в качестве левого +=
операнда операторов и -=
операторов. Эти операторы используются соответственно для присоединения обработчиков событий к событию или удаления обработчиков событий из события, а также модификаторов доступа элемента управления событиями контекстов, в которых разрешены такие операции.
Единственные операции, разрешенные для события по коду, который находится за пределами типа, в котором объявлено это событие, и +=
-=
. Таким образом, хотя такой код может добавлять и удалять обработчики для события, он не может напрямую получить или изменить базовый список обработчиков событий.
В операции формы или , когда x += y
является событием, результатом операции является тип x –= y
(§12.21.5x
в отличие от типа void
назначения, а также другие 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
событие. Концепция создания события в точности соответствует вызову делегата, представленного этим событием. Это позволяет обойтись без особой языковой конструкции для создания событий. Обратите внимание, что перед вызовом делегата предшествует проверка, которая гарантирует, что делегат не имеет значения NULL и что проверка выполняется в локальной копии, чтобы обеспечить безопасность потока.Вне объявления
Button
классаClick
член может использоваться только в левой части+=
и–=
операторов, как и вb.Click += new EventHandler(...);
который добавляет делегат в список вызовов
Click
события иClick –= new EventHandler(...);
, который удаляет делегат из списка вызовов
Click
события.пример конца
При компиляции события, подобного полю, компилятор автоматически создает хранилище для удержания делегата и создает методы доступа для события, добавляющего или удаляющего обработчики событий в поле делегата. Операции добавления и удаления являются потокобезопасным и могут (но не требуются) выполняться при удержании блокировки (§13.13) на содержащего объекта для события экземпляра или System.Type
объекта (§12.8.18) для статического события.
Примечание. Таким образом, объявление события экземпляра формы:
class X { public event D Ev; }
должен быть скомпилирован в что-то эквивалентное:
class X { private D __Ev; // field to hold the delegate public event D Ev { add { /* Add the delegate in a thread safe way */ } remove { /* Remove the delegate in a thread safe way */ } } }
Ссылки на класс
X
Ev
в левой части+=
и–=
операторы вызывают вызовы к добавлению и удалению методов доступа. Все остальные ссылкиEv
компилируются для ссылки на скрытое поле__Ev
(§12.8.7). Имя "__Ev
" является произвольным; скрытое поле может иметь любое имя или ни одно имя вообще.конечная заметка
15.8.3 Методы доступа к событиям
Примечание. Объявления событий обычно не event_accessor_declaration s, как показано в приведенном
Button
выше примере. Например, они могут быть включены, если стоимость хранения одного поля на событие неприемлема. В таких случаях класс может включать event_accessor_declarationи использовать частный механизм для хранения списка обработчиков событий. конечная заметка
Event_accessor_declarations события указывают исполняемые инструкции, связанные с добавлением и удалением обработчиков событий.
Объявления доступа состоят из add_accessor_declaration и remove_accessor_declaration. Каждое объявление метода доступа состоит из добавления или удаления маркера, за которым следует блок. Блок, связанный с add_accessor_declaration, указывает инструкции, выполняемые при добавлении обработчика событий, а блок, связанный с remove_accessor_declaration, указывает инструкции для выполнения при удалении обработчика событий.
Каждый add_accessor_declaration и remove_accessor_declaration соответствует методу с одним параметром значения типа события и возвращаемым типомvoid
. Неявный параметр метода доступа к событиям называется value
. При использовании события в назначении событий используется соответствующий метод доступа к событиям. В частности, если оператор назначения используется +=
, то используется метод доступа к добавлению, а если оператор назначения является –=
методом удаления. В любом случае правый операнды оператора присваивания используется в качестве аргумента для метода доступа к событиям. Блок add_accessor_declaration или remove_accessor_declaration должен соответствовать правилам методовvoid
, описанным в разделе 15.6.9. В частности, return
операторы в таком блоке не допускаются для указания выражения.
Так как метод доступа к событиям неявно имеет параметр с именем value
, это ошибка во время компиляции для локальной переменной или константы, объявленной в методе доступа к событиям, чтобы иметь это имя.
Пример. В следующем коде
class Control : Component { // Unique keys for events static readonly object mouseDownEventKey = new object(); static readonly object mouseUpEventKey = new object(); // Return event handler associated with key protected Delegate GetEventHandler(object key) {...} // Add event handler associated with key protected void AddEventHandler(object key, Delegate handler) {...} // Remove event handler associated with key protected void RemoveEventHandler(object key, Delegate handler) {...} // MouseDown event public event MouseEventHandler MouseDown { add { AddEventHandler(mouseDownEventKey, value); } remove { RemoveEventHandler(mouseDownEventKey, value); } } // MouseUp event public event MouseEventHandler MouseUp { add { AddEventHandler(mouseUpEventKey, value); } remove { RemoveEventHandler(mouseUpEventKey, value); } } // Invoke the MouseUp event protected void OnMouseUp(MouseEventArgs args) { MouseEventHandler handler; handler = (MouseEventHandler)GetEventHandler(mouseUpEventKey); if (handler != null) { handler(this, args); } } }
Control
Класс реализует внутренний механизм хранения событий. МетодAddEventHandler
связывает значение делегата с ключом,GetEventHandler
метод возвращает делегат, связанный с ключом, иRemoveEventHandler
метод удаляет делегат в качестве обработчика событий для указанного события. Предположительно, базовый механизм хранения разработан таким образом, что нет затрат на связывание значения делегата NULL с ключом, и поэтому необработанные события не используют хранилища.пример конца
События статического и экземпляра 15.8.4
Если объявление события включает static
модификатор, событие считается статическим событием. Если модификатор отсутствуетstatic
, событие считается событием экземпляра.
Статическое событие не связано с определенным экземпляром, и это ошибка во время компиляции для ссылки this
на методы доступа статического события.
Событие экземпляра связано с заданным экземпляром класса, и доступ к этому экземпляру можно получить как this
(§12.8.14) в методах доступа этого события.
Различия между статическими и экземплярами рассматриваются далее в §15.3.8.
15.8.5 Виртуальные, запечатанные, переопределения и абстрактные методы доступа
Объявление виртуального события указывает, что методы доступа этого события являются виртуальными. Модификатор virtual
применяется к обоим методом доступа события.
Объявление абстрактного события указывает, что методы доступа события являются виртуальными, но не предоставляют фактическую реализацию методов доступа. Вместо этого не абстрактные производные классы необходимы для предоставления собственной реализации для методов доступа путем переопределения события. Поскольку метод доступа для объявления абстрактного события не предоставляет фактической реализации, он не должен предоставлять event_accessor_declarations.
Объявление события, включающее 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'
| unsafe_modifier // unsafe code support
;
indexer_declarator
: type 'this' '[' parameter_list ']'
| type interface_type '.' 'this' '[' parameter_list ']'
;
indexer_body
: '{' accessor_declarations '}'
| '=>' expression ';'
;
ref_indexer_body
: '{' ref_get_accessor_declaration '}'
| '=>' 'ref' variable_reference ';'
;
unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).
Существует два типа indexer_declaration:
- Первый объявляет индексатор без ссылок. Его значение имеет тип типа. Этот тип индексатора может быть доступен для чтения и /или записи.
- Второй объявляет индексатор с ref-значением. Его значением является variable_reference (§9.5), которые могут быть
readonly
переменными типа. Этот тип индексатора доступен только для чтения.
Indexer_declaration может включать набор атрибутов (§22) и любой из разрешенных видов объявленной специальных возможностей (§15.3.6), new
(§15.3.5), (§15.5). virtual
6.4), override
(§15.6.5), sealed
(§15.6.6abstract
§15.6.7extern
§15.6.8) модификаторов.
Объявления индексатора применяются к тем же правилам, что и объявления методов (§15.6) в отношении допустимых сочетаний модификаторов, при этом одно исключение заключается в том, что static
модификатор не допускается в объявлении индексатора.
Тип объявления индексатора указывает тип элемента индексатора, представленного объявлением.
Примечание. Так как индексаторы предназначены для использования в контекстах элементов массива, тип элемента терминов, определенный для массива, также используется с индексатором. конечная заметка
Если индексатор не является явной реализацией элемента интерфейса, за типом следует ключевое слово this
. Для явной реализации члена интерфейса за типомследует interface_type, ".
" и ключевое слово this
. В отличие от других элементов индексаторы не имеют определяемых пользователем имен.
Parameter_list задает параметры индексатора. Список параметров индексатора соответствует методу (§15.6.2), за исключением того, что должен быть указан хотя бы один параметр, и что this
модификаторы параметров , ref
а out
также модификаторы параметров запрещены.
Тип индексатора и каждого из типов, на которые ссылается parameter_list, должен быть по крайней мере так же доступен, как сам индексатор (§7.5.5).
Indexer_body может состоять из текста оператора (§15.7.1) или тела выражения (§15.6.1). В тексте инструкции accessor_declarations, которая должна быть заключена в маркеры "{
" и "}
", объявите методы доступа (§15.7.3) индексатора. Методы доступа указывают исполняемые инструкции, связанные с чтением и записью элементов индексатора.
В indexer_body текст выражения, состоящий из "=>
", за которым следует выражение E
и точка с запятой, точно эквивалентен тексту { get { return E; } }
инструкции, и поэтому можно использовать только для указания индексаторов только для чтения, где результат метода доступа получается одним выражением.
Ref_indexer_body может состоять из текста инструкции или текста выражения. В тексте инструкции get_accessor_declaration объявляет метод доступа (§15.7.3) индексатора. Метод доступа задает исполняемые инструкции, связанные с чтением индексатора.
В ref_indexer_body текст выражения, состоящий из =>
последующихref
, V
и точка с запятой точно эквивалентны тексту { get { return ref V; } }
инструкции.
Примечание. Несмотря на то, что синтаксис для доступа к элементу индексатора совпадает с тем, что для элемента массива, элемент индексатора не классифицируется как переменная. Таким образом, невозможно передать элемент индексатора в качестве
in
out
аргумента,ref
если индексатор не имеет ссылочного значения и поэтому возвращает ссылку (§9.7). конечная заметка
Parameter_list индексатора определяет подпись (§7.6) индексатора. В частности, сигнатура индексатора состоит из числа и типов его параметров. Тип элемента и имена параметров не являются частью подписи индексатора.
Подпись индексатора должна отличаться от подписей всех остальных индексаторов, объявленных в одном классе.
Когда объявление индексатора включает extern
модификатор, индексатор, как говорят, является внешним индексатором. Поскольку объявление внешнего индексатора не обеспечивает фактической реализации, каждая из accessor_bodyв его accessor_declarations должна быть точкой с запятой.
Пример. В приведенном ниже примере объявляется
BitArray
класс, реализующий индексатор для доступа к отдельным битам в битовом массиве.class BitArray { int[] bits; int length; public BitArray(int length) { if (length < 0) { throw new ArgumentException(); } bits = new int[((length - 1) >> 5) + 1]; this.length = length; } public int Length => length; public bool this[int index] { get { if (index < 0 || index >= length) { throw new IndexOutOfRangeException(); } return (bits[index >> 5] & 1 << index) != 0; } set { if (index < 0 || index >= length) { throw new IndexOutOfRangeException(); } if (value) { bits[index >> 5] |= 1 << index; } else { bits[index >> 5] &= ~(1 << index); } } } }
Экземпляр
BitArray
класса потребляет значительно меньше памяти, чем соответствующееbool[]
(так как каждое значение бывшего занимает только один бит, а не одинbyte
), но разрешает те же операции, что и одноbool[]
.
CountPrimes
Следующий класс использует и классическийBitArray
алгоритм "сить" для вычисления числа праймеров от 2 до заданного максимума:class CountPrimes { static int Count(int max) { BitArray flags = new BitArray(max + 1); int count = 0; for (int i = 2; i <= max; i++) { if (!flags[i]) { for (int j = i * 2; j <= max; j += i) { flags[j] = true; } count++; } } return count; } static void Main(string[] args) { int max = int.Parse(args[0]); int count = Count(max); Console.WriteLine($"Found {count} primes between 2 and {max}"); } }
Обратите внимание, что синтаксис для доступа к элементам объекта
BitArray
точно совпадает с синтаксисомbool[]
.В следующем примере показан класс сетки 26×10 с индексатором с двумя параметрами. Первый параметр должен быть буквой верхнего или нижнего регистра в диапазоне A–Z, а второй — целым числом в диапазоне 0–9.
class Grid { const int NumRows = 26; const int NumCols = 10; int[,] cells = new int[NumRows, NumCols]; public int this[char row, int col] { get { row = Char.ToUpper(row); if (row < 'A' || row > 'Z') { throw new ArgumentOutOfRangeException("row"); } if (col < 0 || col >= NumCols) { throw new ArgumentOutOfRangeException ("col"); } return cells[row - 'A', col]; } set { row = Char.ToUpper(row); if (row < 'A' || row > 'Z') { throw new ArgumentOutOfRangeException ("row"); } if (col < 0 || col >= NumCols) { throw new ArgumentOutOfRangeException ("col"); } cells[row - 'A', col] = value; } } }
пример конца
15.9.2 Индексатор и различия свойств
Индексаторы и свойства очень похожи в концепции, но отличаются следующими способами:
- Свойство определяется его именем, а индексатор определяется сигнатурой.
- Доступ к свойству осуществляется через simple_name (§12.8.4) или member_access (§12.8.7), а элемент индексатора осуществляется через element_access (§12.8.12.3).
- Свойство может быть статическим элементом, в то время как индексатор всегда является членом экземпляра.
- Метод доступа к свойству соответствует методу без параметров, в то время как метод доступа индексатора соответствует методу с тем же списком параметров, что и индексатор.
- Метод доступа набора свойства соответствует методу с одним параметром с именем
value
, а метод доступа к набору индексатора соответствует методу с тем же списком параметров, что и индексатор, а также дополнительный параметр с именемvalue
. - Это ошибка во время компиляции для метода доступа индексатора для объявления локальной переменной или локальной константы с тем же именем, что и параметр индексатора.
- В объявлении переопределения свойств наследуемое свойство обращается с помощью синтаксиса
base.P
, гдеP
находится имя свойства. В объявлении индексатора переопределения наследуемый индексатор обращается с помощью синтаксисаbase[E]
, гдеE
находится разделенный запятыми список выражений. - Отсутствует концепция "автоматически реализованного индексатора". Это ошибка иметь не абстрактный, не внешний индексатор с точкой с запятой accessor_body.
Помимо этих различий, все правила, определенные в §15.7.3, §15.7.5 и §15.7.6 , применяются к средствам доступа индексатора, а также к средствам доступа к свойствам.
При чтении §15.7.3, §15.7.3, §15.7.5 и §15.7.6 также применяется к определенным терминам. В частности, свойство чтения и записи становится индексатором только для чтения, свойство только для чтения становится индексатором только для чтения, а свойство только для записи становится индексатором только для записи.
Операторы 15.10
15.10.1 Общие
Оператор — это член, определяющий смысл оператора выражения, который может применяться к экземплярам класса. Операторы объявляются с помощью operator_declarations:
operator_declaration
: attributes? operator_modifier+ operator_declarator operator_body
;
operator_modifier
: 'public'
| 'static'
| 'extern'
| unsafe_modifier // unsafe code support
;
operator_declarator
: unary_operator_declarator
| binary_operator_declarator
| conversion_operator_declarator
;
unary_operator_declarator
: type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
;
logical_negation_operator
: '!'
;
overloadable_unary_operator
: '+' | '-' | logical_negation_operator | '~' | '++' | '--' | 'true' | 'false'
;
binary_operator_declarator
: type 'operator' overloadable_binary_operator
'(' fixed_parameter ',' fixed_parameter ')'
;
overloadable_binary_operator
: '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^' | '<<'
| right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
;
conversion_operator_declarator
: 'implicit' 'operator' type '(' fixed_parameter ')'
| 'explicit' 'operator' type '(' fixed_parameter ')'
;
operator_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).
Примечание. Операторы префикса логического отрицания (§12.9.4) и постфиксов NULL-прощения операторов (§12.8.9), представленные тем же лексическим токеном (!
), отличаются. Последний не является перегруженным оператором.
конечная заметка
Существует три категории перегруженных операторов: унарные операторы (§15.10.2), двоичные операторы (§15.10.3) и операторы преобразования (§15.10.4).
Operator_body — это точка с запятой, тело блока (§15.6.1) или тело выражения (§15.6.1). Текст блока состоит из блока, который задает инструкции, выполняемые при вызове оператора. Блок должен соответствовать правилам для методов возврата значений, описанных в разделе 15.6.11. Текст выражения состоит из =>
выражения и точки с запятой и обозначает одно выражение для выполнения при вызове оператора.
Для extern
операторов operator_body состоит просто из точки с запятой. Для всех остальных операторов operator_body — это блоковый текст или тело выражения.
Следующие правила применяются ко всем объявлениям операторов:
- Объявление оператора должно включать как модификатор,
public
так иstatic
модификатор. - Параметры оператора не должны иметь модификаторов, отличных от
in
. - Подпись оператора (§15.10.2, §15.10.3, §15.10.4) должна отличаться от подписей всех остальных операторов, объявленных в одном классе.
- Все типы, на которые ссылаются в объявлении оператора, должны быть по крайней мере так же доступны, как сам оператор (§7.5.5).
- Это ошибка для одного модификатора, который будет отображаться несколько раз в объявлении оператора.
Каждая категория операторов накладывает дополнительные ограничения, как описано в следующих подклаузах.
Как и другие члены, операторы, объявленные в базовом классе, наследуются производными классами. Так как объявления операторов всегда требуют класса или структуры, в которой оператор объявляется для участия в подписи оператора, невозможно скрыть оператор, объявленный в производном классе, чтобы скрыть оператор, объявленный в базовом классе. Таким образом, new
модификатор никогда не требуется и поэтому никогда не допускается в объявлении оператора.
Дополнительные сведения об унарных и двоичных операторах см. в статье 12.4.
Дополнительные сведения о операторах преобразования см. в разделе §10.5.
Унарные операторы 15.10.2
Следующие правила применяются к объявлениям унарных операторов, где T
обозначает тип экземпляра класса или структуры, содержащей объявление оператора:
- Унарный
+
,-
!
(только логический отрицание) или~
оператор должен принимать один параметр типаT
илиT?
может возвращать любой тип. - Унарный
++
или--
оператор должен принимать один параметр типаT
илиT?
возвращать тот же тип или тип, производный от него. - Унарный
true
илиfalse
оператор должен принимать один параметр типаT
илиT?
возвращать типbool
.
Сигнатура унарного оператора состоит из маркера оператора (+
, , -
!
, ~
++
, --
true
илиfalse
) и типа одного параметра. Возвращаемый тип не является частью подписи унарного оператора и не является именем параметра.
Для true
унарных false
операторов требуется парное объявление. Ошибка во время компиляции возникает, если класс объявляет один из этих операторов без объявления другого. Операторы true
описаны далее в false
.
Пример. В следующем примере показана реализация и последующее использование оператора++ для целочисленного векторного класса:
public class IntVector { public IntVector(int length) {...} public int Length { get { ... } } // Read-only property public int this[int index] { get { ... } set { ... } } // Read-write indexer public static IntVector operator++(IntVector iv) { IntVector temp = new IntVector(iv.Length); for (int i = 0; i < iv.Length; i++) { temp[i] = iv[i] + 1; } return temp; } } class Test { static void Main() { IntVector iv1 = new IntVector(4); // Vector of 4 x 0 IntVector iv2; iv2 = iv1++; // iv2 contains 4 x 0, iv1 contains 4 x 1 iv2 = ++iv1; // iv2 contains 4 x 2, iv1 contains 4 x 2 } }
Обратите внимание, как метод оператора возвращает значение, созданное путем добавления 1 в операнду, так же, как операторы добавочного и декремента постфикса (§12.8.16), а также операторы добавочного и декремента префикса (§12.9.6). В отличие от C++, этот метод не должен изменять значение операнда непосредственно, так как это нарушает стандартную семантику оператора добавочного префикса (§12.8.16).
пример конца
Двоичные операторы 15.10.3
Следующие правила применяются к объявлениям двоичных операторов, где T
обозначает тип экземпляра класса или структуры, содержащей объявление оператора:
- Двоичный оператор, отличный от смены, должен принимать два параметра, по крайней мере один из которых должен иметь тип
T
илиT?
может возвращать любой тип. - Двоичный
<<
или>>
оператор (§12.11) должен принимать два параметра, первый из которых должен иметь тип или T? и второй из которых должен иметь тип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).
Constructor_declaration может включать набор атрибутов (§22), любой из разрешенных видов объявленной специальных возможностей (§15.3.6extern
§15.6.8). Объявление конструктора не допускается включать один и тот же модификатор несколько раз.
Идентификатор constructor_declarator должен называть класс, в котором объявлен конструктор экземпляра. Если указано другое имя, возникает ошибка во время компиляции.
Необязательный parameter_list конструктора экземпляра применяется к тем же правилам, что и parameter_list метода (§15.6).
this
Поскольку модификатор для параметров применяется только к методам расширения (§15.6.10), ни в parameter_list конструктора не должен содержать this
модификатор. Список параметров определяет сигнатуру (§7.6) конструктора экземпляра и управляет процессом разрешения перегрузки (§12.6.4) выбирает конкретный конструктор экземпляра в вызове.
Каждый из типов, на которые ссылается в parameter_list конструктора экземпляра, должен быть по крайней мере так же доступен, как сам конструктор (§7.5.5).
Необязательный constructor_initializer указывает другой конструктор экземпляра для вызова перед выполнением инструкций, заданных в constructor_body этого конструктора экземпляра. Это описано далее в §15.11.2.
Когда объявление конструктора включает extern
модификатор, конструктор считается внешним конструктором. Поскольку объявление внешнего конструктора не предоставляет фактической реализации, его constructor_body состоит из точки с запятой. Для всех остальных конструкторов constructor_body состоит из обоих конструкторов.
- блок, указывающий операторы для инициализации нового экземпляра класса; или
- текст выражения, состоящий из
=>
выражения и точки с запятой, и обозначает одно выражение для инициализации нового экземпляра класса.
Constructor_body, который является блоком или текстом выражения, точно соответствует блоку метода экземпляра с типом void
возвращаемого значения (§15.6.11).
Конструкторы экземпляров не наследуются. Таким образом, класс не имеет конструкторов экземпляров, отличных от фактически объявленных в классе, за исключением того, что если класс не содержит объявлений конструктора экземпляров, конструктор экземпляра по умолчанию автоматически предоставляется (§15.11.5).
Конструкторы экземпляров вызываются object_creation_expressions (§12.8.17.2) и constructor_initializer s.
Инициализаторы конструктора 15.11.2
Все конструкторы экземпляров (за исключением object
классов) неявно включают вызов другого конструктора экземпляра непосредственно перед constructor_body. Конструктор для неявного вызова определяется constructor_initializer:
- Инициализатор конструктора экземпляра формы
base(
argument_list)
(где argument_list является необязательным) вызывает конструктор экземпляра из прямого базового класса. Этот конструктор выбирается с помощью argument_list и правил разрешения перегрузки в §12.6.4. Набор конструкторов экземпляров-кандидатов состоит из всех конструкторов доступных экземпляров прямого базового класса. Если этот набор пуст или если не удается определить один лучший конструктор экземпляра, возникает ошибка во время компиляции. - Инициализатор конструктора экземпляра формы
this(
argument_list)
(где argument_list является необязательным) вызывает другой конструктор экземпляра из того же класса. Конструктор выбирается с помощью argument_list и правил разрешения перегрузки в §12.6.4. Набор конструкторов экземпляров-кандидатов состоит из всех конструкторов экземпляров, объявленных в самом классе. Если результирующий набор применимых конструкторов экземпляров пуст или если не удается определить один лучший конструктор экземпляра, возникает ошибка во время компиляции. Если объявление конструктора экземпляра вызывается через цепочку одного или нескольких инициализаторов конструкторов, возникает ошибка во время компиляции.
Если конструктор экземпляра не имеет инициализатора конструктора, инициализатор base()
формы неявно предоставляется.
Примечание. Таким образом, объявление конструктора экземпляра формы
C(...) {...}
точно эквивалентен
C(...) : base() {...}
конечная заметка
Область параметров, заданных parameter_list объявления конструктора экземпляра, включает инициализатор конструктора этого объявления. Таким образом, инициализатор конструктора может получить доступ к параметрам конструктора.
Пример:
class A { public A(int x, int y) {} } class B: A { public B(int x, int y) : base(x + y, x - y) {} }
пример конца
Инициализатор конструктора экземпляра не может получить доступ к созданному экземпляру. Поэтому это ошибка во время компиляции для ссылки на это в выражении аргумента инициализатора конструктора, так как это ошибка во время компиляции для выражения аргумента для ссылки на любой член экземпляра через simple_name.
Инициализаторы переменных экземпляра 15.11.3
Если конструктор экземпляра не имеет инициализатора конструктора или имеет инициализатор конструктора формы base(...)
, конструктор неявно выполняет инициализации, указанные variable_initializerполя экземпляра, объявленные в своем классе. Это соответствует последовательности назначений, которые выполняются сразу после входа в конструктор и перед неявным вызовом конструктора прямого базового класса. Инициализаторы переменных выполняются в текстовом порядке, в котором они отображаются в объявлении класса (§15.5.6).
Выполнение конструктора 15.11.4
Инициализаторы переменных преобразуются в инструкции присваивания, и эти инструкции присваиваются перед вызовом конструктора экземпляра базового класса. Это упорядочение гарантирует, что все поля экземпляра инициализированы их инициализаторами переменных перед выполнением любых инструкций, имеющих доступ к этому экземпляру.
Пример: учитывая следующее:
class A { public A() { PrintFields(); } public virtual void PrintFields() {} } class B: A { int x = 1; int y; public B() { y = -1; } public override void PrintFields() => Console.WriteLine($"x = {x}, y = {y}"); }
B()
При создании экземпляраB
экземпляра создается следующий результат:x = 1, y = 0
Значение
x
равно 1, так как инициализатор переменных выполняется перед вызовом конструктора экземпляра базового класса. Однако значениеy
равно 0 (значение по умолчанию)int
так как назначениеy
не выполняется до тех пор, пока конструктор базового класса не возвращается. Полезно рассматривать инициализаторы переменных экземпляров и инициализаторы конструкторов как инструкции, автоматически вставляемые перед constructor_body. Примерclass A { int x = 1, y = -1, count; public A() { count = 0; } public A(int n) { count = n; } } class B : A { double sqrt2 = Math.Sqrt(2.0); ArrayList items = new ArrayList(100); int max; public B(): this(100) { items.Add("default"); } public B(int n) : base(n - 1) { max = n; } }
содержит несколько инициализаторов переменных; он также содержит инициализаторы конструкторов обеих форм (
base
иthis
). Пример соответствует приведенному ниже коду, где каждый комментарий указывает автоматически вставленный оператор (синтаксис, используемый для вызовов автоматически вставленного конструктора, не является допустимым, но просто служит для иллюстрации механизма).class A { int x, y, count; public A() { x = 1; // Variable initializer y = -1; // Variable initializer object(); // Invoke object() constructor count = 0; } public A(int n) { x = 1; // Variable initializer y = -1; // Variable initializer object(); // Invoke object() constructor count = n; } } class B : A { double sqrt2; ArrayList items; int max; public B() : this(100) { B(100); // Invoke B(int) constructor items.Add("default"); } public B(int n) : base(n - 1) { sqrt2 = Math.Sqrt(2.0); // Variable initializer items = new ArrayList(100); // Variable initializer A(n - 1); // Invoke A(int) constructor max = n; } }
пример конца
Конструкторы по умолчанию 15.11.5
Если класс не содержит объявлений конструктора экземпляра, конструктор экземпляра по умолчанию автоматически предоставляется. Этот конструктор по умолчанию просто вызывает конструктор прямого базового класса, как если бы он имел инициализатор конструктора формы base()
. Если класс абстрактен, то объявленная специальные возможности для конструктора по умолчанию защищена. В противном случае объявленный специальный доступ для конструктора по умолчанию является общедоступным.
Примечание. Таким образом, конструктор по умолчанию всегда имеет форму.
protected C(): base() {}
or
public C(): base() {}
где
C
— имя класса.конечная заметка
Если разрешение перегрузки не может определить уникального кандидата для инициализатора конструктора базового класса, возникает ошибка во время компиляции.
Пример. В следующем коде
class Message { object sender; string text; }
Конструктор по умолчанию предоставляется, так как класс не содержит объявлений конструктора экземпляра. Таким образом, пример точно эквивалентен
class Message { object sender; string text; public Message() : base() {} }
пример конца
15.12 Статические конструкторы
Статический конструктор — это член, реализующий действия, необходимые для инициализации закрытого класса. Статические конструкторы объявляются с помощью static_constructor_declaration:
static_constructor_declaration
: attributes? static_constructor_modifiers identifier '(' ')'
static_constructor_body
;
static_constructor_modifiers
: 'static'
| 'static' 'extern' unsafe_modifier?
| 'static' unsafe_modifier 'extern'?
| 'extern' 'static' unsafe_modifier?
| 'extern' unsafe_modifier 'static'
| unsafe_modifier 'static' 'extern'?
| unsafe_modifier 'extern' 'static'
;
static_constructor_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2) доступен только в небезопасном коде (§23).
Static_constructor_declaration может включать набор атрибутов (§22) и модификатор (extern
).
Идентификатор static_constructor_declaration должен называть класс, в котором объявлен статический конструктор. Если указано другое имя, возникает ошибка во время компиляции.
Если объявление статического конструктора включает extern
модификатор, статический конструктор считается внешним статическим конструктором. Поскольку объявление внешнего статического конструктора не предоставляет фактической реализации, его static_constructor_body состоит из точки с запятой. Для всех других объявлений статических конструкторов static_constructor_body состоит из одного из следующих объявлений.
- блок, указывающий инструкции для выполнения для инициализации класса; или
- текст выражения, состоящий из
=>
выражения и точки с запятой, и обозначает одно выражение для инициализации класса.
Static_constructor_body, который является текстом блока или выражения, точно соответствует method_body статического метода с типом void
возвращаемого значения (§15.6.11).
Статические конструкторы не наследуются и не могут вызываться напрямую.
Статический конструктор для закрытого класса выполняется не более одного раза в определенном домене приложения. Выполнение статического конструктора активируется первым из следующих событий, которые происходят в домене приложения:
- Создается экземпляр класса.
- Ссылки на любые статические члены класса.
Если класс содержит Main
метод (§7.1), в котором начинается выполнение, статический конструктор для этого класса выполняется перед вызовом Main
метода.
Чтобы инициализировать новый тип закрытого класса, сначала создается новый набор статических полей (§15.5.2) для конкретного закрытого типа. Каждое из статических полей инициализировано в значение по умолчанию (§15.5.5). Затем для этих статических полей выполняются инициализаторы статических полей (§15.5.6.2). Наконец, выполняется статический конструктор.
Пример: пример
class Test { static void Main() { A.F(); B.F(); } } class A { static A() { Console.WriteLine("Init A"); } public static void F() { Console.WriteLine("A.F"); } } class B { static B() { Console.WriteLine("Init B"); } public static void F() { Console.WriteLine("B.F"); } }
должен производить выходные данные:
Init A A.F Init B B.F
так как выполнение
A
статического конструктора активируется вызовомA.F
, и выполнениеB
статического конструктора активируется вызовомB.F
.пример конца
Можно создать циклические зависимости, позволяющие статическим полям с инициализаторами переменных наблюдаться в состоянии значения по умолчанию.
Пример: пример
class A { public static int X; static A() { X = B.Y + 1; } } class B { public static int Y = A.X + 1; static B() {} static void Main() { Console.WriteLine($"X = {A.X}, Y = {B.Y}"); } }
выводятся следующие выходные данные
X = 1, Y = 2
Для выполнения
Main
метода система сначала запускает инициализатор доB.Y
статического конструктора классаB
.Y
Инициализатор вызываетA
запуск конструктораstatic
, так как значениеA.X
ссылается. Статический конструкторA
, в свою очередь, переходит к вычислению значения, и при этом извлекает значение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).
Finalizer_declaration может включать набор атрибутов(§22).
Идентификатор finalizer_declarator должен называть класс, в котором объявлен метод завершения. Если указано другое имя, возникает ошибка во время компиляции.
Когда объявление метода завершения включает extern
модификатор, метод завершения считается внешним методом завершения. Поскольку объявление внешнего средства завершения не предоставляет фактической реализации, его finalizer_body состоит из точки с запятой. Для всех остальных средств завершения finalizer_body состоит из любого из
- блок, указывающий инструкции, выполняемые для завершения экземпляра класса.
- или текст выражения, состоящий из
=>
выражения и точки с запятой, и обозначает одно выражение для выполнения для завершения экземпляра класса.
Finalizer_body, представляющий собой блок или текст выражения, соответствует точно method_body метода экземпляра с типом возвращаемого void
значения (§15.6.11).
Методы завершения не наследуются. Таким образом, класс не имеет методов завершения, отличных от того, который может быть объявлен в этом классе.
Примечание. Так как метод завершения не требует параметров, его нельзя перегружать, поэтому класс может иметь, по крайней мере, один метод завершения. конечная заметка
Методы завершения вызываются автоматически и не могут вызываться явным образом. Экземпляр становится допустимым для завершения, если он больше не может использовать этот экземпляр. Выполнение средства завершения для экземпляра может произойти в любое время после того, как экземпляр станет допустимым для завершения (§7.9). После завершения экземпляра методы завершения в цепочке наследования экземпляра вызываются по порядку от большинства производных до наименее производных. Средство завершения может выполняться в любом потоке. Дополнительные сведения о правилах, определяющих, когда и как выполняется метод завершения, см. в разделе 7.9.
Пример: выходные данные примера
class A { ~A() { Console.WriteLine("A's finalizer"); } } class B : A { ~B() { Console.WriteLine("B's finalizer"); } } class Test { static void Main() { B b = new B(); b = null; GC.Collect(); GC.WaitForPendingFinalizers(); } }
-
B's finalizer A's finalizer
так как методы завершения в цепочке наследования вызываются в порядке, от большинства производных до наименее производных.
пример конца
Методы завершения реализуются путем переопределения виртуального метода 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 Общие
Элемент функции (§12.6), реализованный с помощью блока итератора (§13.3) называется итератором.
Блок итератора может использоваться в качестве текста элемента функции, если возвращаемый тип соответствующего элемента функции является одним из интерфейсов перечислителя (§15.14.2) или одного из перечисленных интерфейсов (§15.14.3). Это может произойти как method_body, operator_body или accessor_body, в то время как события, конструкторы экземпляров, статические конструкторы и метод завершения не должны быть реализованы как итераторы.
Если член функции реализуется с помощью блока итератора, это ошибка во время компиляции для списка параметров члена функции, указывающего любой in
элемент функции, out
или ref
параметры или параметр ref struct
типа.
Интерфейсы перечислителя 15.14.2
Интерфейсы System.Collections.IEnumerator
универсального интерфейсаSystem.Collections.Generic.IEnumerator<T>
. Для краткости в этом подклаузе и его братьях и сестрах эти интерфейсы ссылаются как IEnumerator
и IEnumerator<T>
соответственно.
Интерфейсы перечисления 15.14.3
Перечисляемые System.Collections.IEnumerable
универсального интерфейсаSystem.Collections.Generic.IEnumerable<T>
. Для краткости в этом подклаузе и его братьях и сестрах эти интерфейсы ссылаются как IEnumerable
и IEnumerable<T>
соответственно.
Тип доходности 15.14.4
Итератор создает последовательность значений всех одинаковых типов. Этот тип называется типом доходности итератора.
- Тип доходности итератора, который возвращает
IEnumerator
илиIEnumerable
являетсяobject
. - Тип доходности итератора, который возвращает
IEnumerator<T>
илиIEnumerable<T>
являетсяT
.
Объекты перечислителя 15.14.5
15.14.5.1 Общие
Если элемент функции, возвращающий тип интерфейса перечислителя, реализуется с помощью блока итератора, вызов элемента функции не сразу выполняет код в блоке итератора. Вместо этого создается и возвращается объект перечислителя. Этот объект инкапсулирует код, указанный в блоке итератора, и выполнение кода в блоке итератора происходит при вызове метода объекта MoveNext
перечислителя. Объект перечислителя имеет следующие характеристики:
- Он реализует и
IEnumerator
,IEnumerator<T>
гдеT
тип доходности итератора. - Он реализует
System.IDisposable
. - Он инициализирован с копией значений аргументов (если таковой) и значения экземпляра, передаваемые члену функции.
- Он имеет четыре потенциальных состояния, до, запуск, приостановку и после, и первоначально находится в состоянии до.
Объект перечислителя обычно является экземпляром класса перечислителя, созданного компилятором, который инкапсулирует код в блоке итератора и реализует интерфейсы перечислителя, но возможны другие методы реализации. Если класс перечислителя создается компилятором, этот класс будет вложен напрямую или косвенно в класс, содержащий член функции, он будет иметь частные специальные возможности, и он будет иметь имя, зарезервированное для использования компилятором (§6.4.3).
Объект перечислителя может реализовать больше интерфейсов, чем указанные выше.
В следующих подклаузах описывается требуемое поведение MoveNext
и Current
Dispose
члены IEnumerator
IEnumerator<T>
реализаций интерфейса, предоставляемых объектом перечислителя.
Объекты перечислителя не поддерживают IEnumerator.Reset
метод. Вызов этого метода приводит System.NotSupportedException
к возникновению ошибки.
15.14.5.2 Метод MoveNext
Метод MoveNext
объекта перечислителя инкапсулирует код блока итератора. Вызов MoveNext
метода выполняет код в блоке итератора и задает Current
свойство объекта перечислителя соответствующим образом. Точное действие, выполняемое в MoveNext
зависимости от состояния объекта перечислителя при MoveNext
вызове:
- Если состояние объекта перечислителя до этого, вызовите
MoveNext
:- Изменяет состояние выполнения.
- Инициализирует параметры блока
this
итератора в значения аргументов и значение экземпляра, сохраненные при инициализации объекта перечислителя. - Выполняет блок итератора от начала до прерывания выполнения (как описано ниже).
- Если выполняется состояние объекта перечислителя, результат вызова
MoveNext
не указан. - Если состояние объекта перечислителя приостановлено, вызов moveNext:
- Изменяет состояние выполнения.
- Восстанавливает значения всех локальных переменных и параметров (включая
this
) значения, сохраненные при выполнении блока итератора, в последний раз приостановлены.Примечание. Содержимое любых объектов, на которые ссылается эти переменные, может измениться с момента предыдущего вызова
MoveNext
. конечная заметка - Возобновляет выполнение блока итератора сразу после инструкции возвращаемого значения, которая вызвала приостановку выполнения и продолжается до прерывания выполнения (как описано ниже).
- Если состояние объекта перечислителя находится после, вызов
MoveNext
возвращает значение false.
При MoveNext
выполнении блока итератора выполнение может быть прервано четырьмя способами: yield return
оператором, yield break
оператором, путем обнаружения конца блока итератора, а также путем исключения, вызываемого и распространяемого из блока итератора.
-
yield return
При обнаружении оператора (§9.4.4.20):- Выражение, заданное в инструкции, вычисляется, неявно преобразуется в тип доходности и назначается
Current
свойству объекта перечислителя. - Выполнение текста итератора приостановлено. Значения всех локальных переменных и параметров (включая
this
) сохраняются, как расположение этойyield return
инструкции.yield return
Если оператор находится в одном или несколькихtry
блоках, связанные блоки, в настоящее время не выполняются связанные блоки. - Состояние объекта перечислителя изменяется на приостановленное.
- Метод
MoveNext
возвращаетсяtrue
вызывающему объекту, указывая, что итерация успешно расширена до следующего значения.
- Выражение, заданное в инструкции, вычисляется, неявно преобразуется в тип доходности и назначается
-
yield break
При обнаружении оператора (§9.4.4.20):-
yield break
Если инструкция находится в одном или несколькихtry
блоках, выполняются связанныеfinally
блоки. - Состояние объекта перечислителя изменяется на после.
- Метод
MoveNext
возвращаетсяfalse
вызывающему объекту, указывая, что итерация завершена.
-
- При обнаружении конца текста итератора:
- Состояние объекта перечислителя изменяется на после.
- Метод
MoveNext
возвращаетсяfalse
вызывающему объекту, указывая, что итерация завершена.
- При возникновении исключения и распространении из блока итератора:
- Соответствующие
finally
блоки в тексте итератора будут выполняться распространением исключений. - Состояние объекта перечислителя изменяется на после.
- Распространение исключений продолжается вызывающим методом
MoveNext
.
- Соответствующие
15.14.5.3 Текущее свойство
Свойство объекта Current
перечислителя влияет на yield return
операторы в блоке итератора.
Если объект перечислителя находится в приостановленном состоянии, значением является значение Current
, заданное предыдущим вызовом MoveNext
. Если объект перечислителя находится в состоянии до, выполнения или после состояния, результат доступа Current
не указан.
Для итератора с типом доходности, кроме object
того, результат доступа Current
через реализацию объекта IEnumerable
перечислителя соответствует реализации Current
перечислителя через реализацию объекта IEnumerator<T>
перечислителя и приведение результата к object
нему.
15.14.5.4 Метод Dispose
Метод Dispose
используется для очистки итерации путем добавления объекта перечислителя в состояние после .
- Если состояние объекта перечислителя до этого, вызов
Dispose
состояния изменится на после. - Если выполняется состояние объекта перечислителя, результат вызова
Dispose
не указан. - Если состояние объекта перечислителя приостановлено, вызовите
Dispose
:- Изменяет состояние выполнения.
- Выполняет все блоки, наконец, как если бы последняя выполненная
yield return
инструкция была операторомyield break
. Если это приводит к возникновению и распространению исключения из текста итератора, состояние объекта перечислителя устанавливается после и исключение распространяется вызывающемуDispose
методу. - Изменяет состояние после.
- Если состояние объекта перечислителя находится после, вызов
Dispose
не влияет.
Объекты перечисления 15.14.6
15.14.6.1 General
Если элемент функции, возвращающий тип перечисленного интерфейса, реализуется с помощью блока итератора, вызов элемента функции не сразу выполняет код в блоке итератора. Вместо этого создается и возвращается перечислимый объект . Метод перечисленного объекта GetEnumerator
возвращает объект перечислителя, который инкапсулирует код, указанный в блоке итератора, и выполнение кода в блоке итератора происходит при MoveNext
вызове метода перечислителя. Перечисленный объект имеет следующие характеристики:
- Он реализует и
IEnumerable
,IEnumerable<T>
гдеT
тип доходности итератора. - Он инициализирован с копией значений аргументов (если таковой) и значения экземпляра, передаваемые члену функции.
Перечисляемый объект обычно является экземпляром класса, созданного компилятором, который инкапсулирует код в блоке итератора и реализует перечисляемые интерфейсы, но возможны другие методы реализации. Если перечисленный класс создается компилятором, этот класс будет вложен напрямую или косвенно в класс, содержащий член функции, он будет иметь частные специальные возможности, и он будет иметь имя, зарезервированное для использования компилятором (§6.4.3).
Перечисленный объект может реализовать больше интерфейсов, чем указанные выше.
Примечание. Например, перечислимый объект также может реализовываться
IEnumerator
иIEnumerator<T>
, что позволяет использовать его как перечисление, так и перечислитель. Как правило, такая реализация возвращает собственный экземпляр (для сохранения выделений) из первого вызоваGetEnumerator
. Последующие вызовыGetEnumerator
, если таковые есть, возвращают новый экземпляр класса, как правило, одного класса, чтобы вызовы разных экземпляров перечислителя не влияли друг на друга. Он не может возвращать тот же экземпляр, даже если предыдущий перечислитель уже перечислил после конца последовательности, так как все будущие вызовы исчерпанного перечислителя должны вызывать исключения. конечная заметка
15.14.6.2 Метод GetEnumerator
Перечисляемый объект предоставляет реализацию GetEnumerator
методов IEnumerable
и IEnumerable<T>
интерфейсов. Два GetEnumerator
метода совместно используют общую реализацию, которая получает и возвращает доступный объект перечислителя. Объект перечислителя инициализируется со значениями аргументов и значением экземпляра, сохраненным при инициализации объекта перечисления, но в противном случае объект перечислителя работает, как описано в разделе §15.14.5.
15.15 Асинхронные функции
15.15.1 General
Метод (§15.6) или анонимная функция (§12.19) с модификатором называется асинхронной async
. Как правило, асинхронный термин используется для описания любой функции, которая имеет async
модификатор.
Это ошибка во время компиляции для списка параметров асинхронной функции, чтобы указать любой in
out
, или ref
параметры или любой ref struct
параметр типа.
Return_type асинхронного метода должен быть либо void
задачи. Для асинхронного метода, создающего значение результата, тип задачи должен быть универсальным. Для асинхронного метода, который не создает результирующий значение, тип задачи не должен быть универсальным. Такие типы называются в этой спецификации «TaskType»<T>
и «TaskType»
соответственно. Тип System.Threading.Tasks.Task
и типы стандартной библиотеки, созданные из System.Threading.Tasks.Task<TResult>
них, являются типами задач, а также классом, структурой или типом интерфейса, связанным с типом построителя задач с помощью атрибута.System.Runtime.CompilerServices.AsyncMethodBuilderAttribute
Такие типы называются в этой спецификации и «TaskBuilderType»<T>
«TaskBuilderType»
. Тип задачи может иметь не более одного параметра типа и не может быть вложен в универсальный тип.
Асинхронный метод, возвращающий тип задачи, считается возвращающим задачу.
Типы задач могут отличаться в их точном определении, но с точки зрения языка тип задачи находится в одном из состояний , неполных, успешных или неисправных. Сбой задачи записывает соответствующее исключение. Успешно записывает«TaskType»<T>
результат типа T
. Типы задач являются ожидаемыми, поэтому задачи могут быть операнды выражений await (§12.9.8).
Пример. Тип
MyTask<T>
задачи связан с типомMyTaskMethodBuilder<T>
построителя задач и типомAwaiter<T>
ожидания:using System.Runtime.CompilerServices; [AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))] class MyTask<T> { public Awaiter<T> GetAwaiter() { ... } } class Awaiter<T> : INotifyCompletion { public void OnCompleted(Action completion) { ... } public bool IsCompleted { get; } public T GetResult() { ... } }
пример конца
Тип построителя задач — это класс или тип структуры, соответствующий определенному типу задачи (§15.15.2). Тип построителя задач должен точно соответствовать объявленной доступности соответствующего типа задачи.
Примечание. Если тип задачи объявлен, соответствующий тип построителя также должен быть объявлен
internal
internal
и определен в той же сборке. Если тип задачи вложен внутри другого типа, то тип задачи buider также должен быть вложен в тот же тип. конечная заметка
Асинхронная функция имеет возможность приостановить оценку с помощью выражений await (§12.9.8) в тексте. Позже оценка может быть возобновлена в момент приостановки выражения ожидания с помощью делегата возобновления. Делегат возобновления имеет тип System.Action
, и при его вызове оценка вызова асинхронной функции возобновляется из выражения await, в котором оно осталось.
Текущий вызывающий вызов асинхронной функции — это исходный вызывающий объект, если вызов функции никогда не был приостановлен или последний вызывающий делегат возобновления в противном случае.
Шаблон построителя типов задач 15.15.2
Тип построителя задач может иметь не более одного параметра типа и не может быть вложен в универсальный тип. Тип построителя задач должен иметь следующие члены (для типов построителя задач, SetResult
не имеющих параметров) с объявленными public
специальными возможностями:
class «TaskBuilderType»<T>
{
public static «TaskBuilderType»<T> Create();
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine;
public void SetStateMachine(IAsyncStateMachine stateMachine);
public void SetException(Exception exception);
public void SetResult(T result);
public void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine;
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine;
public «TaskType»<T> Task { get; }
}
Компилятор должен создать код, использующий TaskBuilderType для реализации семантики приостановки и возобновления оценки асинхронной функции. Компилятор должен использовать taskBuilderType следующим образом:
-
«TaskBuilderType».Create()
вызывается для создания экземпляра taskBuilderType, названногоbuilder
в этом списке. -
builder.Start(ref stateMachine)
вызывается для связывания построителя с экземпляромstateMachine
компьютера, созданного компилятором, .- Построитель должен вызвать
stateMachine.MoveNext()
либо вStart()
, либо послеStart()
того, как он вернулся для продвижения государственного компьютера.
- Построитель должен вызвать
- После
Start()
возвратаasync
метод вызываетbuilder.Task
задачу, возвращаемую из асинхронного метода. - Каждый вызов будет
stateMachine.MoveNext()
продвигать компьютер состояния. - Если компьютер состояния завершается успешно,
builder.SetResult()
вызывается с возвращаемым значением метода, если таковой имеется. - В противном случае, если исключение
e
создается на компьютере состояния,builder.SetException(e)
вызывается. - Если компьютер состояния достигает
await expr
выражения,expr.GetAwaiter()
вызывается. - Если средство ожидания реализует
ICriticalNotifyCompletion
иIsCompleted
имеет значение false, то вызываетсяbuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)
компьютер состояния.-
AwaitUnsafeOnCompleted()
должен вызыватьсяawaiter.UnsafeOnCompleted(action)
с вызовомAction
, который вызываетсяstateMachine.MoveNext()
после завершения ожидания.
-
- В противном случае вызывается компьютер
builder.AwaitOnCompleted(ref awaiter, ref stateMachine)
состояния.-
AwaitOnCompleted()
должен вызыватьсяawaiter.OnCompleted(action)
с вызовомAction
, который вызываетсяstateMachine.MoveNext()
после завершения ожидания.
-
-
SetStateMachine(IAsyncStateMachine)
Может вызываться реализацией, созданнойIAsyncStateMachine
компилятором, чтобы определить экземпляр построителя, связанного с экземпляром компьютера состояния, особенно в тех случаях, когда конечный компьютер реализуется как тип значения.- Если построитель вызывает, вызовет
stateMachine.SetStateMachine(stateMachine)
stateMachine
экземпляр построителя,builder.SetStateMachine(stateMachine)
связанный сstateMachine
.
- Если построитель вызывает, вызовет
Примечание. Для обоих
SetResult(T result)
и«TaskType»<T> Task { get; }
, соответственно, параметр и аргумент должны быть преобразованыT
в идентификатор. Это позволяет построителю типов задач поддерживать такие типы, как кортежи, в которых два типа, которые не одинаковы, являются преобразованными удостоверениями. конечная заметка
15.15.3. Оценка возвращаемой задачи асинхронной функции
Вызов возвращаемой задачи асинхронной функции приводит к созданию экземпляра возвращаемого типа задачи. Это называется задачей возврата асинхронной функции. Задача изначально находится в неполном состоянии.
Затем текст асинхронной функции вычисляется до тех пор, пока он не будет приостановлен (путем достижения выражения await) или завершается, когда элемент управления точками возвращается вызывающему объекту вместе с задачей возврата.
Когда текст асинхронной функции завершается, задача возврата перемещается из неполного состояния:
- Если тело функции завершается в результате достижения инструкции возврата или конца тела, любое значение результата записывается в задаче возврата, которая помещается в состояние успешного выполнения.
- Если тело функции завершается из-за неученного
OperationCanceledException
, исключение записывается в задаче возврата, которая помещается в отмененное состояние. - Если тело функции завершается в результате любого другого неухваченного исключения (§13.10.6), исключение записывается в задаче возврата, которая помещается в состояние сбоя .
15.15.4 Оценка асинхронной функции, возвращающей пустоту
Если возвращаемый тип асинхронной функции void
является, оценка отличается от приведенного выше способа: поскольку задача не возвращается, функция вместо этого сообщает о завершении и исключениях в контексте синхронизации текущего потока. Точное определение контекста синхронизации зависит от реализации, но представляет собой представление "где" текущий поток выполняется. Контекст синхронизации уведомляется при оценке void
возвращающей асинхронной функции, успешно завершается или вызывает неуверенное исключение.
Это позволяет контексту отслеживать, сколько void
асинхронных функций выполняется под ним, и решить, как распространять исключения, исходящие из них.
ECMA C# draft specification