15.1 一般
类是可能包含数据成员(常量和字段)、函数成员(方法、属性、事件、索引器、运算符、实例构造函数、终结器和静态构造函数)和嵌套类型的数据结构。 类类型支持继承,即派生类可以扩展和专用基类的机制。
结构(§16)和接口(§18)的成员类似于类,但存在某些限制。 此子句定义类和类成员的声明。 结构与接口的子句根据类类型中的相应声明定义这些类型的限制。
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由一组可选的属性(§23)组成,后跟一组可选的class_modifier(§15.2.2),后跟可选partial修饰符(§15.2.7),后跟关键字class和命名类的标识符, 后跟可选type_parameter_list(§15.2.3),后跟可选class_base规范(§15.2.4),后跟可选集type_parameter_constraints_clauses (§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 (§24.2)仅在不安全的代码(§24)中可用。
同一修饰符在类声明中多次出现属于一个编译时错误。
new允许在嵌套类上使用修饰符。 它指定该类会隐藏同名的继承成员,如 §15.3.5 中所述。
new 修饰符如果出现在非嵌套类声明上,会导致编译时错误。
public和protectedinternalprivate修饰符控制类的可访问性。 根据类声明的上下文,某些修饰符可能不允许(§7.5.2)。
当分部类型声明(§15.2.7)包括访问权限规范(通过public、protected、internal和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,或者 - 在 形式的
T中,namespace_or_type-name 为 (typeof(T))。
允许primary_expression(§12.8)引用静态类,如果
在任何其他上下文中,引用静态类是编译时错误。
注意:例如,静态类用作基类、成员的构成类型(§15.3.7)、泛型类型参数或类型参数约束是错误的。 同样,静态类不能用于数组类型、新表达式、强制转换表达式、is 表达式、as 表达式、
sizeof表达式或默认值表达式。 尾注
15.2.3 类型参数
类型参数是一个简单的标识符,表示为创建构造类型而提供的类型参数的占位符。 相比之下,类型参数(§8.4.2)是在创建构造类型时替换类型参数的类型。
type_parameter_list
: '<' decorated_type_parameter (',' decorated_type_parameter)* '>'
;
decorated_type_parameter
: attributes? type_parameter
;
type_parameter 的定义见于 §8.5。
类声明中的每个类型参数在该类的声明空间(§7.3)中定义一个名称。 因此,它不能与该类的另一类型参数或在该类中声明的成员同名。 类型参数不能与类型本身同名。
如果两个分部泛型类型声明(在同一程序中)具有相同的完全限定名称(包括类型参数数量的 generic_dimension_specifier (§12.8.18)),则这两个声明会用于同一未绑定泛型类型 (§7.8.3)。 两个此类分部类型声明应按顺序为每个类型参数指定相同的名称。
15.2.4. 类基规范
15.2.4.1 通用
类声明可能包括 class_base 规范,该规范定义类的直接基类和类直接实现的接口(§19)。
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)中声明的嵌套类型(§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 用作直接或间接基类(§23.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[]>>和Aobject。结束示例
除类 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.C又依赖于A(其直接外围类),而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, §19.2)。 基接口本身不能是类型参数,尽管它可能涉及范围中的类型参数。
示例:以下代码演示类如何实现和扩展构造类型:
class C<U, V> {} interface I1<V> {} class D : C<string, int>, I1<string> {} class E<T> : C<int, T>, I1<T> {}结束示例
接口实现在 §19.6 中进一步讨论。
15.2.5 类型参数约束
泛型类型和方法声明可以选择通过包括 type_parameter_constraints_clauses 来指定类型参数约束。
type_parameter_constraints_clause
: 'where' type_parameter ':' type_parameter_constraints
;
type_parameter_constraints
: primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
| secondary_constraints (',' constructor_constraint)?
| constructor_constraint
;
primary_constraint
: class_type nullable_type_annotation?
| 'class' nullable_type_annotation?
| 'struct'
| 'notnull'
| 'unmanaged'
;
secondary_constraint
: interface_type nullable_type_annotation?
| type_parameter nullable_type_annotation?
;
secondary_constraints
: secondary_constraint (',' secondary_constraint)*
;
constructor_constraint
: 'new' '(' ')'
;
每个 type_parameter_constraints_clause 都由令牌 where、类型参数的名称、冒号以及该类型参数的约束列表组成。 每个类型参数最多可以有一个 where 子句,子 where 句可以按任意顺序列出。 像属性访问器中的get和set标记一样,where标记也不是关键字。
子句中 where 给定的约束列表可以包括以下任何组件,顺序如下:单个主约束、一个或多个辅助约束和构造函数约束 new()。
主要约束可为类类型、引用类型约束class、值类型约束struct、不可为 null 约束notnull或非托管类型约束unmanaged。 类类型和引用类型约束可包括 nullable_type_annotation。
辅助约束可为 interface_type 或 type_parameter,并可选择性后跟 nullable_type_annotation。 如果存在 nullable_type_annotation,则表示允许类型参数为对应于满足该约束的不可为 null 引用类型。
引用类型约束指定用于类型参数的类型参数应为引用类型。 已知为引用类型的所有类类型、接口类型、委托类型、数组类型和类型参数(如下所述)都满足此约束。
类类型、引用类型约束和辅助约束可包含可为 null 的类型注释。 类型参数上是否存在此注释表示类型参数的可为 null 性预期结果:
- 如果约束不包含可为 null 的类型注释,则类型参数应为不可为 null 的引用类型。 如果类型参数为可以为 null 的引用类型,编译器可能会发出警告。
- 如果约束包含可为 null 的类型注释,则不可为 null 的引用类型和可为 null 的引用类型均满足该约束。
类型参数的可为 null 性不必与类型参数的可为 null 性相匹配。 如果类型参数的可空性与类型实参的可空性不匹配,编译器可能会发出警告。
注意:若要指定类型参数是可为 null 的引用类型,请不要将可为 null 的类型注释添加为约束(使用
T : class或T : BaseClass),但在整个泛型声明中使用T?,以指示类型参数的相应可为 null 引用类型。 尾注
可为 null 的类型批注 ?只能用于具有值类型约束的类型参数、没有 nullable_type_annotation的引用类型约束或没有 nullable_type_annotation的类类型约束。
示例:以下示例演示类型参数的可为 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 的引用类型的类型参数,但编译器可能会产生诊断警告。
由于 notnull 不是关键字,因此 primary_constraint 非空约束始终与 class_type在语法上不明确。 出于兼容性原因,如果名称的名称查找(notnull)成功,应被视为一种class_type名称。 否则,应将其视为非 null 约束。
示例:以下类演示了对不同约束使用各种类型参数,指示编译器可能发出的警告。
#nullable enable public class C { } public class A<T> where T : notnull { } public class B1<T> where T : C { } public class B2<T> where T : C? { } class Test { static void M() { // nonnull constraint allows nonnullable struct type argument A<int> x1; // possible warning: nonnull constraint prohibits nullable struct type argument A<int?> x2; // nonnull constraint allows nonnullable class type argument A<C> x3; // possible warning: nonnull constraint prohibits nullable class type argument A<C?> x4; // nonnullable base class requirement allows nonnullable class type argument B1<C> x5; // possible warning: nonnullable base class requirement prohibits nullable class type argument B1<C?> x6; // nullable base class requirement allows nonnullable class type argument B2<C> x7; // nullable base class requirement allows nullable class type argument B2<C?> x8; } }
值类型约束指定用于类型参数的类型参数应为不可为 null 的值类型。 具有值类型约束的所有不可为 null 的结构类型、枚举类型和类型参数都满足此约束。 请注意,虽然分类为值类型,但可以为 null 的值类型(§8.3.12)不满足值类型约束。 具有值类型约束的类型参数不应具有constructor_constraint,但它可以用作另一个具有constructor_constraint的类型参数的类型实参。
注意:
System.Nullable<T>类型指定了T的非 null 值类型约束。 因此,形式为T??和Nullable<Nullable<T>>的递归构造类型是被禁止的。 尾注
非托管类型约束指定用于类型参数的类型参数应为不可为 null 的非托管类型(§8.8)。
由于 unmanaged 不是关键字,因此在 primary_constraint 中,非托管约束在语法上总会与 class_type 产生混淆。 出于兼容性原因,如果名称查找(§12.8.4)成功,则将其视为unmanaged。 否则,它将被视为非托管约束。
指针类型永不允许为类型参数,且不满足任何类型约束(即使是非托管类型),尽管它们当前属于非托管类型。
如果约束是类类型、接口类型或类型参数,则该类型指定用于该类型参数的每个类型参数应支持的最小“基类型”。 每当使用构造类型或泛型方法时,编译时会根据类型参数的约束条件检查类型参数。 提供的类型参数应满足 §8.4.5 中所述的条件。
class_type约束应满足以下规则:
- 该类型应为类类型。
- 类型不应为
sealed. - 类型不应为下列类型之一:
System.Array或System.ValueType。 - 类型不应为
object. - 给定类型参数的最多一个约束可能是类类型。
指定为 interface_type 约束的类型应满足以下规则:
- 该类型应为接口类型。
- 在给定
where子句中,不应多次指定类型。
在任一情况下,约束都可能涉及关联类型的任何类型参数或方法声明作为构造类型的一部分,并且可能涉及要声明的类型。
指定为类型参数约束的任何类或接口类型,其可访问性至少应等同于所声明的泛型类型或方法(§7.5.5)。
指定为 type_parameter 约束的类型应满足以下规则:
- 该类型应为类型参数。
- 在给定
where子句中,不应多次指定类型。
此外,类型参数依赖项关系图中不应有周期,其中依赖项是以下定义的可传递关系:
- 如果将类型参数
T用作类型参数S的约束,则S依赖于T。 - 如果类型参数
S依赖于类型参数T,并且T依赖于类型参数U,那么S依赖于U。
鉴于此关系,类型参数(直接或间接)依赖于自身属于一种编译时错误。
任何约束应在依赖类型参数之间保持一致。 如果类型参数 S 依赖于类型参数 T ,则:
-
T不应具有值类型约束。 那么,T有效地被密封,S将被强制为与T相同的类型,从而消除了对两个类型参数的需求。 - 如果
S具有值类型约束,则T不应具有 class_type 约束。 - 如果
S具有class_type约束A,并且T具有class_type约束B,那么应当存在从A到B的标识转换或隐式引用转换,或从B到A的隐式引用转换。 - 如果
S还依赖于类型参数U并且U具有class_type约束,A并且T具有class_type约束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_constraint 为 struct 或 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嵌套类型Outer.Inner,则Cₓ为嵌套类型Outerₓ.Innerₓ。 - 如果是
CCₓ具有类型参数G<A¹, ..., Aⁿ>A¹, ..., Aⁿ的构造类型Cₓ,则为构造类型G<A¹ₓ, ..., Aⁿₓ>。 - 如果
C是数组类型E[],那么Cₓ就是数组类型Eₓ[]。 - 如果是
C动态的,Cₓ则为object. - 否则
Cₓ为C。
类型参数的定义如下:
让我们 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的有效基类是集合中覆盖范围最广的类型(R)。 如果集没有包含类型,则有效基类为Tobject. 一致性规则确保存在最包含的类型。
如果类型参数是一个方法类型参数,其约束是从基方法继承的,则有效基类在类型替换后计算。
这些规则可确保有效的基类始终是 class_type。
类型参数的定义如下:
- 如果
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 的引用类型约束,则该类型参数已知为不可为 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),在关键字classstructinterface和关键字之前具有特殊含义。 (分部类型可能包含分部方法声明(§15.6.9)。
部分类型声明的每个部分应包含一个partial修饰符,并且应在与其他部分相同的命名空间或包含类型中声明。
partial修饰符指示类型声明的其他部分可能存在于其他位置,但存在此类附加部件并不是必需的;它对于包含修饰符的类型的唯一partial声明有效。 它仅对包含基类或已实现接口的分部类型的一个声明有效。 但是,基类或已实现接口的所有声明应匹配,包括任何指定类型参数的可为 null 性。
部分类型的所有部分应一起编译,以便可以在编译时合并这些部件。 具体来说,部分类型不允许扩展已编译的类型。
可以通过使用 partial 修饰符在多个部分中声明嵌套类型。 通常,包含类型也使用 partial 声明,嵌套类型的每个部分则分别声明在包含类型的不同位置。
示例:以下分部类在两个部分实现,它们驻留在不同的编译单元中。 第一部分是由数据库映射工具生成的,而第二部分是手动创作的。
public partial class Customer { private int id; private string name; private string address; private List<Order> orders; public Customer() { ... } } // File: Customer2.cs public partial class Customer { public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted); public bool HasOutstandingOrders() => orders.Count > 0; }当上述两个部分一起编译时,生成的代码的行为就像类已编写为单个单元一样,如下所示:
public class Customer { private int id; private string name; private string address; private List<Order> orders; public Customer() { ... } public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted); public bool HasOutstandingOrders() => orders.Count > 0; }结束示例
§23.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),而 class_declaration 立即包含的 type_parameter 和 class_member_declaration 会将新成员引入此声明空间。 以下规则将应用于 class_member_declaration:
实例构造函数、终结器和静态构造函数的名称应与立即封闭的类相同。 所有其他成员的名称均应不同于立即封闭类的名称。
类声明type_parameter_list中类型参数的名称应与同一type_parameter_list中所有其他类型参数的名称不同,并且与类的名称和类的所有成员的名称不同。
类型的名称应不同于在同一类中声明的所有非类型成员的名称。 如果两个或更多类型声明共享相同的完全限定名称,则声明应具有
partial修饰符(§15.2.7),这些声明组合在一起以定义单个类型。
注意:由于类型声明的完全限定名称对类型参数的数目进行编码,因此,只要类型参数数量不同,两个不同的类型可以共享相同的名称。 尾注
常量、字段、属性或事件的名称应不同于同一类中声明的所有其他成员的名称。
方法的名称应不同于同一类中声明的所有其他非方法的名称。 此外,方法的签名(§7.6)应不同于同一类中声明的所有其他方法的签名,同一类中声明的两个方法不得具有唯
in一不同的签名,out并且ref。实例构造函数的签名应不同于在同一类中声明的所有其他实例构造函数的签名,在同一类中声明的两个构造函数不应具有唯
refout一不同的签名。索引器的签名应不同于在同一类中声明的所有其他索引器的签名。
运算符的签名应不同于在同一类中声明的所有其他运算符的签名。
类(§15.3.4)的继承成员不是类的声明空间的一部分。
注意:因此,允许派生类声明与继承成员同名或具有相同签名的成员(它实际上会隐藏继承的成员)。 尾注
在多个部分(§15.2.7)中声明的某种类型的成员集合是每个部分中声明的成员的并集。 类型声明的所有部分的主体共享相同的声明空间(§7.3),每个成员(§7.7)的范围扩展到所有部件的主体。 任何成员的可访问性域始终包括封闭类型的所有部分;在一个部分声明的私有成员可从另一个部分自由访问。 除非该成员具有 partial 修饰符,否则在类型的多个部分中声明同一成员是编译时错误。
示例:
partial class A { int x; // Error, cannot declare x more than once partial void M(); // Ok, defining partial method declaration partial class Inner // Ok, Inner is a partial type { int y; } } partial class A { int x; // Error, cannot declare x more than once partial void M() { } // Ok, implementing partial method declaration partial class Inner // Ok, Inner is a partial type { int z; } }结束示例
字段初始化顺序在 C# 代码中可能很重要,并且提供了一些保证,如 §15.5.6.1 中所述。 否则,类型中成员的排序很少重要,但在与其他语言和环境交互时可能很重要。 在这些情况下,未定义在多个部件中声明的类型中的成员的顺序。
15.3.2 实例类型
每个类声明都有一个关联的实例类型。 对于泛型类声明,实例类型是通过从类型声明创建构造类型(§8.4)而形成的,其中每个提供的类型参数都是相应的类型参数。 由于实例类型使用类型参数,因此只能用于类型参数在范围内的位置;也就是说,在类声明中。 实例类型是类声明中编写的代码的类型 this 。 对于非泛型类,实例类型只是声明的类。
示例:下面显示了多个类声明及其实例类型:
class A<T> // instance type: A<T> { class B {} // instance type: A<T>.B class C<U> {} // instance type: A<T>.C<U> } class D {} // instance type: D结束示例
15.3.3. 构造类型的成员
构造类型的非继承成员是通过替换(针对成员声明中的每个 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)base_class_specification 的相应 type_argument 来进行转换。
示例:
class B<U> { public U F(long index) {...} } class D<T> : B<T[]> { public T G(string s) {...} }在上面的代码中,构造类型
D<int>具有一个非继承成员intG(string s),该成员是通过将类型参数int替换为类型参数T获得的。D<int>还具有来自类声明B的继承成员。 首先,通过在基类规范B<int[]>中用D<int>替换int,确定TB<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在符合以下条件时被视为可用:首先,该成员是可访问的,其次,没有其他已经隐藏MM的继承的可访问成员 N。 隐式隐藏继承的成员不被视为错误,但编译器发出警告,除非派生类成员的声明包括一个 new 修饰符,以明确指示派生成员旨在隐藏基成员。 如果嵌套类型的一个或多个部分声明(§15.2.7)包含 new 修饰符,则嵌套类型隐藏可用继承成员时不会发出警告。
如果在声明中包含的new修饰符没有隐藏可用的继承成员,则会发出相关警告。
15.3.6. 访问修饰符
class_member_declaration可以具有任何一种允许的声明可访问性(§7.5.2):、public、protected internal、protected、private protected,或internal。 除了 protected internal 和 private protected 组合之外,指定多个访问修饰符是编译时错误。 当class_member_declaration不包含任何访问修饰符时,假设默认为private。
15.3.7. 关系人类型
在成员声明中使用的类型称为 该成员的构成类型 。 可能的构成类型是常量、字段、属性、事件或索引器的类型、方法或运算符的返回类型,以及方法、索引器、运算符或实例构造函数的参数类型。 成员的构成类型应至少与该成员本身(§7.5.5)一样可访问。
15.3.8. 静态成员和实例成员
类的成员是静态成员或实例成员。
注意:一般来说,将静态成员视为属于类和实例成员属于对象(类实例)很有用。 尾注
当字段、方法、属性、事件、运算符或构造函数声明包含 static 修饰符时,它将声明静态成员。 此外,常量或类型声明隐式声明静态成员。 静态成员具有以下特征:
- 当在
M(§12.8.7)形式中引用静态成员 时,E.M应表示具有成员E的类型。E表示实例属于一种编译时错误。 - 非泛型类中的静态字段确切标识一个存储位置。 无论创建了多少个非泛型类的实例,静态字段只有一个副本。 每个不同的封闭构造类型(§8.4.3)都有自己的静态字段集,而不考虑封闭构造类型的实例数。
- 静态函数成员(方法、属性、事件、运算符或构造函数)不在特定实例上运行,在此类函数成员中引用此方法是编译时错误。
当字段、方法、属性、事件、索引器、构造函数或终结器声明不包含静态修饰符时,它将声明实例成员。 (实例成员有时称为非静态成员。实例成员具有以下特征:
- 在
M(§12.8.7)形式中引用实例成员时,E.M应表示具有成员E的类型实例。 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 是嵌套类型声明(包括任何 generic_dimension_specifier (§12.8.18) 的非限定名 (§7.8.2))。
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 访问权限
嵌套类型及其包含类型与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的实例,并将其自己的 this 传递给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 常规
为了方便基础 C# 运行时实现,对于作为属性、事件或索引器的每个源成员声明,实现应根据成员声明的类型、名称及其类型(§15.3.10.2、§15.3.10.3、§15.3.10.3、§15.3.10.4)保留两个方法签名。 如果程序声明其签名与在同一范围内声明的成员保留的签名匹配,则属于一种编译时错误,且即使基础运行时实现不会使用这些保留对象也是如此。
保留名称不引入声明,因此它们不参与成员检索。 但是,声明的关联保留方法签名确实会参与继承 (§15.3.4),且可以使用 new 修饰符来隐藏 (§15.3.5)。
注意:这些名称的预留有三个用途:
- 若要允许基础实现使用普通标识符作为方法名称,以便获取或设置对 C# 语言功能的访问权限。
- 若要允许其他语言使用普通标识符作为方法名称进行互操作,以便获取或设置对 C# 语言功能的访问权限。
- 为了帮助确保一个符合编译器接受的源被另一个编译器接受,使保留成员名称的具体细节在所有 C# 实现中保持一致。
尾注
终结器的声明 (§15.13) 也会导致签名予以保留 (§15.3.10.5)。
某些名称保留为用作运算符方法名称(§15.3.10.6)。
15.3.10.2. 为属性保留的成员名称
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. 为事件保留的成员名称
void add_E(T handler);
void remove_E(T handler);
15.3.10.4 为索引器保留的成员名称
对于附带参数列表 且类型为 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可能包括一组属性(§23)、new修饰符(§15.3.5),以及任何一种允许的声明辅助功能(§15.3.6)。 属性和修饰符适用于constant_declaration语句声明的所有成员。 即使常量被视为静态成员, constant_declaration 既不需要也不允许 static 修饰符。 同一修饰符在常量声明中出现多次是错误的。
constant_declaration的类型指定声明引入的成员的类型。 该类型会后跟 constant_declarator (§13.6.3) 的列表,且每个列表均会引入一个新成员。
constant_declarator包含一个标识符,该标识符命名成员,后跟一个“=”令牌,后跟一个提供成员值的constant_expression(§12.24)。
常量声明中指定的 type 应为 sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool、string、enum_type 或 reference_type。 每个 constant_expression 应生成一个目标类型或一种类型的值,该类型可以通过隐式转换(§10.2)转换为目标类型。
常 量的类型 应至少与常量本身(§7.5.5.5)一样可访问。
常量的值是在表达式中使用simple_name(§12.8.4)或member_access(§12.8.7)获取的。
常量本身可以参与 constant_expression。 因此,可以在需要 constant_expression的任何构造中使用常量。
注意:此类构造的示例包括
case标签、goto case语句、enum成员声明、属性和其他常量声明。 尾注
注意:如 §12.24 中所述, constant_expression 是可在编译时完全计算的表达式。 由于创建 reference_type(
string除外)的非 null 值的唯一方法是应用new运算符,并且由于new中不允许使用 运算符,因此除 之外string的常量的唯一可能值为null。 尾注
如果需要常量值的符号名称,但在常量声明中不允许该值的类型,或者当constant_expression在编译时无法计算该值时,可以改用只读字段(§15.5.3)。
注意:版本控制语义
const和readonly不同 (§15.5.3.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 (§24.2)仅在不安全的代码(§24)中可用。
field_declaration可能包括一组属性(§23)、new修饰符(§15.3.5)、四个访问修饰符(§15.3.6)的有效组合和修饰static符(§15.5.2)。 此外,field_declaration可能包括readonly修饰符(§15.5.3)或修饰符(§15.5.4),但不能同时包括两者。 属性和修饰符适用于由field_declaration声明的所有成员。 同一修饰符在field_declaration中出现多次是错误的。
field_declaration的类型指定声明引入的成员的类型。 该类型后面是一个variable_declarator列表,其中每个都会引入一个新成员。 变量声明符由一个命名该成员的标识符组成,后面可以选择跟一个“”标记和一个用于提供该成员初始值的变量初始化器(=)。
字段的类型应至少与字段本身(§7.5.5.5)一样可访问。
字段的值是在表达式中使用simple_name(§12.8.4)、member_access(§12.8.7)或base_access(§12.8.15)获取的。 使用 赋值 修改非只读字段的值(§12.22)。 可以使用后缀递增和递减运算符(§12.8.16)和前缀增量和递减运算符(§12.9.7)获取和修改非只读字段的值。
声明多个字段的字段声明等效于具有相同属性、修饰符和类型的单个字段的多个声明。
示例:
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 修饰符时,声明引入的字段是 实例字段。 静态字段和实例字段是 C# 支持的多种变量(§9)中的两种,有时它们分别称为静态变量和实例变量。
如 §15.3.8 中所述,类的每个实例都包含一组完整的类实例字段,而每个非泛型类或封闭构造类型只有一组静态字段,而不管类或封闭构造类型的实例数如何。
15.5.3. 只读字段
15.5.3.1 常规
当 field_declaration 包含 readonly 修饰符时,此声明引入的字段为只读字段。 直接赋值到只读字段只能作为该声明的一部分或在同一类的实例构造函数或静态构造函数中发生。 (在这些上下文中,可多次分配只读字段。)具体而言,仅在以下上下文中才允许直接赋值给只读字段:
- 在 variable_declarator 中,而它会通过在声明中包含 variable_initializer 来引入该字段。
- 对于实例字段,在包含字段声明的类的实例构造函数中,不包括本地函数和匿名函数,并且仅在所构造的实例上。 对于静态字段,在静态构造函数或包含字段声明的类中的静态字段或属性初始值设定项中,不包括本地函数和匿名函数。 它们也是唯一可有效将只读字段作为输出或引用参数来传递的上下文。
尝试分配给只读字段或在任何其他上下文中将其作为输出或引用参数传递是编译时错误。
15.5.3.2 对常量使用静态只读字段
当需要常量值的符号名称时,静态只读字段非常有用,但在 const 声明中不允许该值的类型或编译时无法计算值时。
示例:在以下代码中
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成员不能声明为 const 成员,因为它们的值无法在编译时计算。 但是,将它们声明为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。 - 具有类型为 、、
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对主线程可见,因此允许主线程从字段result中读取值 0。 声明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_initializer。 对于静态字段,变量初始值设定项对应于类初始化期间执行的赋值语句。 对于实例字段,变量初始值设定项对应于创建类实例时执行的赋值语句。
示例:示例
class Test { static double x = Math.Sqrt(2.0); int i = 100; string s = "Hello"; static void Main() { Test a = new Test(); Console.WriteLine($"x = {x}, i = {a.i}, s = {a.s}"); } }生成输出
x = 1.4142135623730951, i = 100, s = Hello因为在执行静态字段初始值设定项时会对
x进行赋值,而对i和s的赋值会出现在实例字段初始值设定项执行时。结束示例
§15.5.5 中所述的默认值初始化针对所有字段(包括具有变量初始值设定项的字段)进行。 因此,初始化类时,该类中的所有静态字段首先初始化为默认值,然后以文本顺序执行静态字段初始值设定项。 同样,创建类的实例时,该实例中的所有实例字段首先初始化为其默认值,然后实例字段初始值设定项按文本顺序执行。 如果同一类型的多个分部类型声明中存在字段声明,则未指定各部分的顺序。 但是,在每个部分内,字段初始值设定项按顺序执行。
可以观察具有变量初始值设定项的静态字段的默认值状态。
示例:但是,强烈建议不要将其作为样式问题。 示例
class Test { static int a = b + 1; static int b = a + 1; static void Main() { Console.WriteLine($"a = {a}, b = {b}"); } }展示了此行为。 尽管
a和b有循环定义,但程序仍然有效。 它导致了输出a = 1, b = 2因为静态字段
a和b在执行其初始值设定项之前会被初始化为0(int的默认值)。 运行a初始化程序时,b的值为零,因此a被初始化为1。 当b的初始值设定项运行时,变量 a 的值已经是1,因此b被初始化为2。结束示例
15.5.6.2 静态字段初始化
某一类的静态字段变量初始值设定项对应于一系列赋值,而这些赋值会按它们在类声明中出现的文本顺序执行 (§15.5.6.1)。 在分部类中,“文本顺序”的含义由 §15.5.6.1 指定。 如果类中存在静态构造函数(§15.12),则在执行该静态构造函数之前,将立即执行静态字段初始值设定项。 否则,静态字段初始值设定项会在首次使用该类的静态字段之前在依赖于实现的时间执行。
示例:示例
class Test { static void Main() { Console.WriteLine($"{B.Y} {A.X}"); } public static int F(string s) { Console.WriteLine(s); return 1; } } class A { public static int X = Test.F("Init A"); } class B { public static int Y = Test.F("Init B"); }可能会生成以下任一输出:
Init A Init B 1 1或输出:
Init B Init A 1 1因为
X的初始值设定项和Y的初始值设定项的执行可按任一顺序进行;它们仅被限制为在引用这些字段之前执行。 但是,在示例中:class Test { static void Main() { Console.WriteLine($"{B.Y} {A.X}"); } public static int F(string s) { Console.WriteLine(s); return 1; } } class A { static A() {} public static int X = Test.F("Init A"); } class B { static B() {} public static int Y = Test.F("Init B"); }输出应为:
Init B Init A 1 1因为静态构造函数的执行规则(如§15.12中定义)规定,
B的静态构造函数(因此也包括B的静态字段初始值设定项)应在A的静态构造函数和字段初始值设定项之前运行。结束示例
15.5.6.3 实例字段初始化
类的实例字段变量初始值设定项对应于在进入该类的任何一个实例构造函数(§15.11.3)时立即执行的赋值序列。 在分部类中,“文本顺序”的含义由 §15.5.6.1 指定。 变量初始值设定项以在类声明(§15.5.6.1)中显示的文本顺序执行。 类实例的创建和初始化过程在 §15.11 中进一步介绍。
实例字段的变量初始值设定项不能引用正在创建的实例。 因此,在变量初始值设定项中引用 this 属于一种编译时错误,因为变量初始值设定项通过 simple_name 来引用任意实例成员属于编译时错误。
示例:在以下代码中
class A { int x = 1; int y = x + 1; // Error, reference to instance member of this }
y的变量初始值设定项会导致编译时错误,因为它会引用正在创建的实例的成员。结束示例
15.6 方法
15.6.1 常规
§15.6 及其子项涵盖类中的方法声明。 该文本通过有关在结构(§16.4)和接口(§19.4.3)中声明方法的信息进行扩充。
方法是实现对象或类可执行的计算或操作的成员。 方法使用 method_declaration 声明:
method_declaration
: attributes? method_modifiers return_type method_header method_body
| attributes? ref_method_modifiers ref_kind ref_return_type method_header
ref_method_body
;
method_modifiers
: method_modifier* 'partial'?
;
ref_kind
: 'ref'
| 'ref' 'readonly'
;
ref_method_modifiers
: ref_method_modifier*
;
method_header
: member_name '(' parameter_list? ')'
| member_name type_parameter_list '(' parameter_list? ')'
type_parameter_constraints_clause*
;
method_modifier
: ref_method_modifier
| 'async'
;
ref_method_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| 'readonly' // direct struct members only
| unsafe_modifier // unsafe code support
;
return_type
: ref_return_type
| 'void'
;
ref_return_type
: type
;
member_name
: identifier
| interface_type '.' identifier
;
method_body
: block
| '=>' null_conditional_invocation_expression ';'
| '=>' expression ';'
| ';'
;
ref_method_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
语法说明:
- unsafe_modifier (§24.2)仅在不安全的代码(§24)中可用。
- 当识别method_body时,如果null_conditional_invocation_expression和表达式替代方案均适用,则应选择前者。
注意:此处的替代项的重叠和优先级只是为了描述性便利;可以详细说明语法规则以删除重叠。 ANTLR 和其他语法系统采用相同的便利性,因此 method_body 自动具有指定的语义。 尾注
method_declaration可能包括一组属性(§23)和一种允许的声明辅助功能(§15.3.6)、(§15.3.5)、new(§15.6.3)、(§15.6.3)、(§15.6.3) static15.6.4、 virtual (override)、 (sealed)、 (abstract)、 (extern(async)。 此外,method_declaration直接包含在struct_declaration中时,可能包括readonly修饰符(§16.4.12)。
如果以下所有内容都为 true,则声明具有修饰符的有效组合:
- 声明包括访问修饰符(§15.3.6)的有效组合。
- 声明不会多次包含同一修饰符。
- 声明最多包含以下修饰符之一:
static、virtual和override。 - 声明最多包含以下修饰符之一:
new和override。 - 如果声明包含
abstract修饰符,则声明不包含以下任何修饰符:static、virtual、sealed或extern。 - 声明可以包含
abstract修饰override符,以便抽象成员可以重写虚拟成员。 - 如果声明包含
private修饰符,则声明不包含以下任何修饰符:virtual、override或abstract。 - 如果声明包含
sealed修饰符,则声明还包括override修饰符。 - 如果声明包含
partial修饰符,则它不包含以下任何修饰符:new、、public、、protected、internal、privatevirtualsealedoverride或。abstractextern
方法根据返回的内容(如果有)进行分类:
- 如果
ref存在,则该方法按引用返回并返回一个变量引用,该引用可以是可选的只读。 - 否则,如果return_type
void为,则该方法返回为 no-value 且不返回值; - 否则,该方法属于按值返回且会返回值。
按值返回或无值返回方法声明的 return_type 指定了该方法返回的结果(如有)的类型。 只有不返回值的方法可以包含 partial 修饰符(§15.6.9)。 如果声明包含 async 修饰符,则 return_type 应为 void,或者方法按值返回时,其返回类型为 任务类型(§15.14.1)。
按引用返回方法声明的 ref_return_type 指定了该方法返回的 variable_reference 所引用的变量的类型。
泛型方法是其声明包括 type_parameter_list的方法。 这指定方法的类型参数。 可选 type_parameter_constraints_clause指定类型参数的约束。
泛型 method_declaration(无论是带有 override 修饰符的,还是用于显式接口成员实现的)分别从被重写的方法或接口成员继承类型参数约束。 此类声明只能有type_parameter_constraints_clause包含primary_constraint,classstruct并且此上下文中的含义分别在 §15.6.5 和 §19.6.2 中定义,用于替代方法和显式接口实现。
member_name指定方法的名称。 除非该方法是显式接口成员实现(§19.6.2), 否则member_name 只是 标识符。
对于显式接口成员实现,member_name由interface_type接着是“.”和一个identifier组成。 在这种情况下,声明不得包含除(可能) extern 或 async以外的任何修饰符。
可选 parameter_list 指定方法的参数(§15.6.2)。
return_type或ref_return_type,以及方法parameter_list中引用的每个类型,应至少与方法本身(§7.5.5)一样可访问。
按值返回或无值返回方法的 method_body 是一个分号、块主体或表达式主体。 块正文由一个 块组成,该块指定要在调用该方法时执行的语句。 表达式正文由 => 组成,后跟 null_conditional_invocation_expression 或 表达式,以及分号,表示调用方法时要执行的单个表达式。
对于抽象和外部方法, method_body 只包含分号。 对于分部方法,method_body 可能由分号、块主体或表达式主体组成。 对于所有其他方法,method_body 为块主体或表达式主体。
如果method_body由分号组成,则声明不应包含async修饰符。
按引用返回方法的 ref_method_body 是一个分号、块主体或表达式主体。 块正文由一个 块组成,该块指定要在调用该方法时执行的语句。 表达式主体由 =>、后跟 ref、variable_reference 和一个分号组成,且表示在调用此方法时要进行求值的单个 variable_reference。
对于抽象和外部方法, ref_method_body 只包含分号;对于所有其他方法, ref_method_body 是块体或表达式体。
名称、类型参数数和方法的参数列表定义方法的签名(§7.6)。 具体而言,方法的签名包括其名称、其类型参数的数目以及数字、parameter_mode_modifier s(§15.6.2.1)及其参数的类型。 返回类型不是方法签名的一部分,也不是参数的名称、类型参数的名称或约束。 当参数类型引用方法的类型参数时,类型参数(而不是类型参数的名称)的序号位置用于类型等效。
方法的名称应不同于同一类中声明的所有其他非方法的名称。 此外,一个方法的签名应与同一类中声明的所有其他方法的签名不同,并且在同一类中声明的两个方法,其签名不能仅因in、out和ref的差异而不同。
方法的type_parameter在整个method_declaration中均在作用范围内,可以用于在return_type或ref_return_type、method_body或ref_method_body中形成类型,以及在type_parameter_constraints_clause中使用,但不能用于attributes中。
所有参数和类型参数应具有不同的名称。
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?
| parameter_mode_modifier? 'this'
;
parameter_mode_modifier
: 'ref'
| 'out'
| 'in'
;
parameter_array
: attributes? 'params' array_type identifier
;
参数列表由一个或多个逗号分隔的参数组成,其中只有最后一个 参数可能是parameter_array。
fixed_parameter由一组可选属性(§23)组成;可选in、out或refthis修饰符;类型;标识符;和可选default_argument。 每个 fixed_parameter 都声明具有给定名称的给定类型的参数。
this修饰符将该方法指定为扩展方法,并且仅在非泛型非嵌套静态类中静态方法的第一个参数上允许。 如果参数是struct类型或受约束为struct的类型参数,this修饰符可以与ref修饰符或in修饰符组合,但不能与out修饰符组合。 扩展方法在 §15.6.10 中进一步介绍。 具有默认参数的固定参数称为可选参数,而没有默认参数的固定参数是必需参数。 必需的参数不应出现在parameter_list中的可选参数之后。
具有ref或outthis修饰符的参数不能具有default_argument。 输入参数可能具有 default_argument。
default_argument中的表达式应为下列表达式之一:
- 一个 constant_expression
- 形式为
new S()的表达式,其中S是值类型 - 形式为
default(S)的表达式,其中S是值类型
expression 应由标识或可为 null 的转换隐式转换为此参数的类型。
如果实现分部方法声明(§15.6.9)、显式接口成员实现(§19.6.2)、单参数索引器声明(§15.9)或运算符声明(§15.10.1)中出现可选参数,编译器应发出警告,因为这些成员永远不能以允许省略参数的方式调用。
parameter_array由一组可选的属性(§23)、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 ) { }在参数列表中,
M是必需参数,i是必需的值参数,ref、d、b和s是可选值参数,o是参数数组。结束示例
方法声明为参数和类型参数创建单独的声明空间(§7.3)。 名称由类型参数列表和方法的参数列表引入此声明空间。 方法的正文(如果有)被视为嵌套在此声明空间中。 方法声明空间的两个成员具有相同名称是错误的。
方法调用 (§12.8.10.2) 会创建特定于该调用的方法的参数和局部变量的副本,且调用的参数列表会将值或变量引用赋给新创建的参数。 在某一方法的 block 中,参数可由 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 中所述,
inout和ref修饰符是方法签名的一部分,但params修饰符不是。 尾注
15.6.2.2. 值参数
不带修饰符的参数称为值参数。 值参数是一个局部变量,它从方法调用中提供的相应参数中获取其初始值。
有关明确赋值规则,请参阅 §9.2.5。
方法调用中的相应参数应是隐式转换为参数类型的表达式(§10.2)。
允许给值参数分配新值的方法。 此类分配仅影响值参数表示的本地存储位置,它们对方法调用中给定的实际参数没有影响。
15.6.2.3. 按引用传递参数
15.6.2.3.1 常规
输入、输出与引用参数属于按引用传递参数。 按引用参数是一种局部引用变量 (§9.7);初始引用是从方法调用中提供的相应参数获取的。
注意:可以使用 ref 赋值 (
= ref) 运算符来更改按引用参数的引用对象。
当参数是按引用方式传递时,方法调用中的相应参数应由相应的关键字in、ref或out,后跟与参数类型相同的变量引用(第9.5节)组成。 但是,当此参数为 in 参数时,参数可能为表达式,而对于该表达式存在从参数表达式到相应参数的类型的隐式转换 (§10.2)。
引用参数不允许用于声明为迭代器 (§15.15) 或异步函数 (§15.14) 的函数。
在采用多个按引用参数的方法中,多个名称可以表示相同的存储位置。
15.6.2.3.2 输入参数
使用 in 修饰符声明的参数是输入 参数。 与输入参数对应的参数是方法调用点存在的变量,或者是方法调用中由实现(§12.6.2.2.3)创建的变量。 有关明确赋值规则,请参阅 §9.2.8。
修改输入参数的值是编译时错误。
注意:输入参数的主要用途是提高效率。 当方法参数的类型是大型结构(在内存要求方面),在调用该方法时,避免复制参数的整个值非常有用。 输入参数允许方法引用内存中的现有值,同时防止这些值受到不需要的更改。 尾注
15.6.2.3.3 参考参数
使用 ref 修饰符声明的参数是 引用参数。 有关明确赋值规则,请参阅 §9.2.6。
示例:示例
class Test { static void Swap(ref int x, ref int y) { int temp = x; x = y; y = temp; } static void Main() { int i = 1, j = 2; Swap(ref i, ref j); Console.WriteLine($"i = {i}, j = {j}"); } }生成输出
i = 2, j = 1在
Swap中调用Main时,x表示i,y表示j。 因此,此调用的效果是交换i和j的值。结束示例
示例:在以下代码中
class A { string s; void F(ref string a, ref string b) { s = "One"; a = "Two"; b = "Three"; } void G() { F(ref s, ref s); } }在
F中调用G会将引用传递给s和a的b。 因此,对于该调用,名称s、a和b都引用相同的存储位置,三个赋值都修改实例字段s。结束示例
struct对于类型,在实例方法、实例访问器(§12.2.1)或具有构造函数初始值设定项的实例构造函数中,this关键字的行为与结构类型的引用参数(§12.8.14)完全相同。
15.6.2.3.4 输出参数
使用 out 修饰符声明的参数是 输出参数。 有关明确赋值规则,请参阅 §9.2.7。
声明为分部方法(§15.6.9)的方法不应具有输出参数。
注意:输出参数通常用于生成多个返回值的方法。 尾注
示例:
class Test { static void SplitPath(string path, out string dir, out string name) { int i = path.Length; while (i > 0) { char ch = path[i - 1]; if (ch == '\\' || ch == '/' || ch == ':') { break; } i--; } dir = path.Substring(0, i); name = path.Substring(i); } static void Main() { string dir, name; SplitPath(@"c:\Windows\System\hello.txt", out dir, out name); Console.WriteLine(dir); Console.WriteLine(name); } }该示例生成输出:
c:\Windows\System\ hello.txt请注意,
dir和name变量在传递给SplitPath之前可以未分配,并且在调用后被视为已明确分配。结束示例
15.6.2.4 参数数组
使用 params 修饰符声明的参数是参数数组。 如果参数列表包含参数数组,则它应为列表中的最后一个参数,并且它应为单维数组类型。
示例:类型
string[]和string[][]可用作参数数组的类型,但类型string[,]不能。 结束示例
注意:无法将
params修饰符与修饰符in合并,out或者ref。 尾注
参数数组允许在方法调用中通过以下两种方式之一指定参数:
- 为参数数组提供的参数可以是隐式转换为参数数组类型的单个表达式(§10.2)。 在这种情况下,参数数组的行为与值参数类似。
- 或者,调用可以为参数数组指定零个或多个参数,其中每个参数都是隐式可转换为参数数组的元素类型的表达式(§10.2)。 在这种情况下,调用会创建参数数组类型的实例,其长度对应于参数数,使用给定参数值初始化数组实例的元素,并使用新创建的数组实例作为实际参数。
除了在调用中允许可变数量的参数外,参数数组与相同类型的值参数 (§15.6.2.2. 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[]由调用创建,并且数组的单个元素使用给定的参数值(它本身是对 anobject[]的引用)进行初始化。结束示例
15.6.3 静态和实例方法
当方法声明包含 static 修饰符时,该方法据说是静态方法。 当不存在 static 修饰符时,该方法称为实例方法。
静态方法不在特定实例上操作,并且在静态方法中引用 this 会导致编译时错误。
实例方法会对类的给定实例进行操作,且该实例可作为 this 进行访问 (§12.8.14)。
静态成员和实例成员之间的差异在 §15.3.8 中进一步讨论。
15.6.4 虚拟方法
当实例方法声明包含虚拟修饰符时,该方法称为虚拟方法。 当不存在虚拟修饰符时,该方法称为 非虚拟方法。
非虚拟方法的实现是固定的:无论该方法是在声明它的类的实例上调用还是派生类的实例上调用,实现都是相同的。 相比之下,虚拟方法的实现可由派生类取代。 取代继承的虚拟方法的实现的过程被称为替代该方法 (§15.6.5)。
在虚拟方法调用中, 进行调用的实例的运行时类型 决定了要调用的实际方法实现。 在非虚拟方法调用中, 实例的编译时类型 是确定因素。 确切地说,当在具有编译时类型和运行时类型的NA 实例上使用参数列表C调用命名R的方法(其中或RC派生自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。
如果重写声明不满足以下所有条件,则会发生编译时错误:
- 可按如上所述找到替代的基方法。
- 只有一个这样的替代的基方法。 仅当基类类型是构造类型时,此限制才有效,其中类型参数的替换使两个方法的签名相同。
- 替代的基方法是一种虚拟、抽象或替代方法。 换句话说,被重写的基类方法不能是静态的或非虚拟的。
- 替代的基方法不属于封装的方法。
- 替代的基方法的返回类型与替代方法之间存在标识转换。
- 替代声明和替代的基方法具有相同的声明的可访问性。 换句话说,重写声明无法更改虚拟方法的可访问性。 但是,如果替代的基方法在内部受到保护,且它在与包含替代声明的程序集不同的程序集中声明,则应保护替代声明的声明的可访问性。
-
type_parameter_constraints_clause 只能由应用于
class的struct或 primary_constraint 主要约束组成,根据继承的约束,这些类型参数分别为引用类型或值类型。 重写方法签名中形如T?的任何类型,其中T是类型参数,解释如下:- 如果为类型参数
class添加了T约束,则T?是可空引用类型;否则 - 无论是没有添加约束,还是添加了
struct约束,对于类型参数TT?都是可为 null 的值类型。
- 如果为类型参数
示例:下面演示重写规则如何适用于泛型类:
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> }结束示例
示例:以下示例演示了当涉及类型参数时重写规则的工作方式:
#nullable enable class A { public virtual void Foo<T>(T? value) where T : class { } public virtual void Foo<T>(T? value) where T : struct { } } class B: A { public override void Foo<T>(T? value) where T : class { } public override void Foo<T>(T? value) where T : struct { } }如果没有类型参数约束
where T : class,则具有引用类型参数的基方法无法被重写。 结束示例
重写声明可使用 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调用会调用A中声明的 PrintFields 方法。 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。
抽象方法声明引入了新的虚拟方法,但不提供该方法的实现。 相反,非抽象派生类需要通过重写该方法来提供自己的实现。 抽象方法 method_body 只包含分号。
抽象方法声明仅在抽象类(§15.2.2.2)和接口(§19.4.3)中得到允许。
示例:在以下代码中
public abstract class Shape { public abstract void Paint(Graphics g, Rectangle r); } public class Ellipse : Shape { public override void Paint(Graphics g, Rectangle r) => g.DrawEllipse(r); } public class Box : Shape { public override void Paint(Graphics g, Rectangle r) => g.DrawRect(r); }该
Shape类定义可自行绘制的几何形状对象的抽象概念。 此方法Paint是抽象的,因为对于形状的抽象概念没有有意义的回退实现。 这些Ellipse和Box类是具体的Shape实现。 由于这些类是非抽象的,因此需替代Paint方法并提供实际实现。结束示例
base_access 引用抽象方法属于一种编译时错误 (§12.8.15)。
示例:在以下代码中
abstract class A { public abstract void F(); } class B : A { // Error, base.F is abstract public override void F() => base.F(); }针对
base.F()调用,会报告编译时错误,因为它引用了抽象方法。结束示例
允许抽象方法声明替代虚拟方法。 这允许抽象类强制在派生类中重新实现方法,并使该方法的原始实现不可用。
示例:在以下代码中
class A { public virtual void F() => Console.WriteLine("A.F"); } abstract class B: A { public abstract override void F(); } class C : B { public override void F() => Console.WriteLine("C.F"); }类
A声明虚拟方法,类B使用抽象方法替代此方法,类C重写抽象方法以提供自己的实现。结束示例
15.6.8 外部方法
当方法声明包含 extern 修饰符时,该方法称为 外部方法。 外部方法在外部实现,通常使用 C# 以外的语言。 由于外部方法声明不提供实际实现,因此外部方法的方法正文仅包含分号。 外部方法不应是泛型方法。
实现与外部方法链接的机制是由实现定义的。
示例:以下示例演示如何使用
extern修饰符和DllImport属性:class Path { [DllImport("kernel32", SetLastError=true)] static extern bool CreateDirectory(string name, SecurityAttribute sa); [DllImport("kernel32", SetLastError=true)] static extern bool RemoveDirectory(string name); [DllImport("kernel32", SetLastError=true)] static extern int GetCurrentDirectory(int bufSize, StringBuilder buf); [DllImport("kernel32", SetLastError=true)] static extern bool SetCurrentDirectory(string name); }结束示例
15.6.9. 分部方法
当方法声明包含 partial 修饰符时,该方法称为部方法。 分部方法只能声明为分部类型的成员(§15.2.7),并且受到许多限制。
分部方法可以在类型声明的一部分定义,并在另一部分实现。 实现是可选的;如果没有部件实现分部方法,则分部方法声明及其所有调用都将从由部件组合生成的类型声明中删除。
分部方法不应定义访问修饰符;它们是隐式私有的。 其返回类型应为 void,其参数不应为输出参数。 仅当标识符 partial 出现在 关键字之前时,标识符才会在方法声明中被识别为上下文关键字 (void)。 分部方法无法显式实现接口方法。
有两种类型的分部方法声明:如果方法声明的主体是分号,则声明据说是 定义分部方法声明。 如果主体不是分号,则声明据说会实现分部方法声明。 在类型声明的各个部分,只有一个定义具有给定签名的部分方法声明,并且最多只能有一个使用给定签名实现分部方法声明。 如果给定了实现分部方法声明,应存在相应的定义分部方法声明,并且声明应匹配如下所述:
- 声明应具有相同的修饰符(尽管不一定按相同顺序)、方法名称、类型参数数和参数数。
- 声明中的相应参数应具有相同的修饰符(尽管不一定按相同顺序)和相同的类型,或标识可转换类型(类型参数名称中的模数差异)。
- 声明中的对应类型参数应具有相同的约束(类型参数名称中的模数差异)。
实现分部方法声明可以出现在与相应定义分部方法声明相同的部分。
只有定义的分部方法会参与重载解析。 因此,无论是否给出实现声明,调用表达式均可能会解析为分部方法的调用。 由于分部方法始终返回 void,因此此类调用表达式始终为表达式语句。 此外,由于分部方法是 private隐式的,因此此类语句将始终出现在声明分部方法的类型声明的某个部分内。
注意:定义和实现分部方法声明的匹配定义不需要参数名称匹配。 使用命名参数(§12.6.2.1)时,这可能会产生令人惊讶但定义明确的行为。 例如,假定在一个文件中存在
M的定义分部方法声明,并在另一个文件中存在实现分部方法声明:// File P1.cs: partial class P { static partial void M(int x); } // File P2.cs: partial class P { static void Caller() => M(y: 0); static partial void M(int y) {} }无效,因为调用使用了来自方法实现中的参数名称,而不是定义部分方法声明中的参数名称。
尾注
如果分部类型声明没有任何部分包含给定分部方法的实现声明,则调用该声明的任何表达式语句都只是从组合类型声明中删除。 因此,调用表达式(包括任何子表达式)在运行时不起作用。 分部方法本身也会被删除,并且不会是组合类型声明的成员。
如果给定分部方法存在实现声明,则保留分部方法的调用。 分部方法产生的方法声明与实现分部方法声明类似,但以下情况除外:
不包括
partial修饰符。生成的方法声明中的属性是定义和实现分部方法声明的组合属性(按未指定的顺序)。 重复项不会被删除。
针对所生成方法声明的参数的属性是定义与实现分部方法声明的相应参数的组合属性,且未指定顺序。 重复项不会被删除。
如果为分部方法 M提供了定义声明而不是实现声明,则适用以下限制:
从
M创建委托属于一种编译时错误 (§12.8.17.5)。在匿名函数中引用
M并将其转换为表达式树类型时,造成编译时错误(§8.6)。作为调用
M的一部分发生的表达式不会影响明确的赋值状态(§9.4),这可能会导致编译时错误。M不能是应用程序(§7.1)的入口点。
分部方法可用于允许类型声明的一部分自定义另一部分的行为,例如工具生成的部分。 请考虑以下分部类声明:
partial class Customer
{
string name;
public string Name
{
get => name;
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
partial void OnNameChanging(string newName);
partial void OnNameChanged();
}
如果此类在未包含任何其他部件的情况下进行编译,分部方法声明及其调用指令将被删除,生成的合并类声明将等效于以下内容:
class Customer
{
string name;
public string Name
{
get => name;
set => name = value;
}
}
但是,假设给出了另一个部分,而它提供了分部方法的实现声明:
partial class Customer
{
partial void OnNameChanging(string newName) =>
Console.WriteLine($"Changing {name} to {newName}");
partial void OnNameChanged() =>
Console.WriteLine($"Changed to {name}");
}
然后,生成的组合类声明将等效于以下内容:
class Customer
{
string name;
public string Name
{
get => name;
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
void OnNameChanging(string newName) =>
Console.WriteLine($"Changing {name} to {newName}");
void OnNameChanged() =>
Console.WriteLine($"Changed to {name}");
}
15.6.10 扩展方法
当方法的第一个参数包含 this 修饰符时,该方法称为 扩展方法。 扩展方法只能在非泛型非嵌套静态类中声明。 扩展方法的第一个参数受到限制,如下所示:
- 仅当输入参数具有值类型时,它才可能是一个输入参数。
- 它可能只是一个引用参数,如果它具有值类型或约束为结构的泛型类型。
- 它不应是输出参数。
- 它不应为指针类型。
示例:下面是声明两个扩展方法的静态类的示例:
public static class Extensions { public static int ToInt32(this string s) => Int32.Parse(s); public static T[] Slice<T>(this T[] source, int index, int count) { if (index < 0 || count < 0 || source.Length - index < count) { throw new ArgumentException(); } T[] result = new T[count]; Array.Copy(source, index, result, 0, count); return result; } }结束示例
扩展方法是常规静态方法。 此外,如果其封闭静态类在范围内,可以使用接收器表达式作为第一个参数,使用实例方法调用语法(§12.8.10.3)调用扩展方法。
示例:以下程序使用上面声明的扩展方法:
static class Program { static void Main() { string[] strings = { "1", "22", "333", "4444" }; foreach (string s in strings.Slice(1, 2)) { Console.WriteLine(s.ToInt32()); } } }
Slice方法可用于string[]上,ToInt32方法可用于string上,因为它们已被声明为扩展方法。 程序的含义与以下相同,使用普通静态方法调用:static class Program { static void Main() { string[] strings = { "1", "22", "333", "4444" }; foreach (string s in Extensions.Slice(strings, 1, 2)) { Console.WriteLine(Extensions.ToInt32(s)); } } }结束示例
15.6.11. 方法主体
方法声明的方法主体由块主体、表达式主体或分号组成。
抽象和外部方法声明不提供方法实现,因此其方法主体只包含分号。 对于任何其他方法,方法正文是一个块(§13.3),其中包含调用该方法时要执行的语句。
方法的有效返回类型是void,如果返回类型是 void 或者如果方法是异步的,并且返回类型是 «TaskType»(§15.14.1)。 否则,非异步方法的有效返回类型就是其自身的返回类型,而返回类型为«TaskType»<T>(§15.14.1)的异步方法的有效返回类型是T。
当方法的有效返回类型是 void 且该方法具有块正文时, return 块中的语句(§13.10.5)不应指定表达式。 如果无效方法的块执行正常完成(即,控制权从方法主体的末尾流出),则该方法只会返回到其调用方。
当方法 void 的有效返回类型是且该方法具有表达式主体时,表达式 E 应为 statement_expression,并且该正文与窗体 { E; }的块体完全等效。
对于返回值方法(§15.6.1),该方法正文中的每个返回语句应指定可隐式转换为有效返回类型的表达式。
对于按引用返回方法 (§15.6.1),该方法主体中的每个返回语句均应指定一个表达式,而其类型为有效返回类型的表达式,且具有内容为 caller-context 的 ref-safe-context (§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的方法会导致编译时错误,因为控制可以流出方法正文的末尾。 和GH方法是正确的,因为所有可能的执行路径都以指定返回值的返回语句结尾。 方法I正确,因为它的正文等效于只包含一个返回语句的块。结束示例
15.7 属性
15.7.1 常规
属性是一个成员,它提供对对象或类的特征的访问权限。 属性的示例包括字符串的长度、字体大小、窗口的标题以及客户的名称。 属性是字段的自然扩展 - 两者都是具有关联类型的命名成员,访问字段和属性的语法是相同的。 不过,与字段不同的是,属性不指明存储位置。 相反,属性包含访问器,用于指定在读取或写入属性值时要执行的语句。 因此,属性提供一种机制,用于将操作与对象或类特征的读取和写入相关联;此外,它们允许计算此类特征。
使用property_declaration语法声明属性:
property_declaration
: attributes? property_modifier* type member_name property_body
| attributes? property_modifier* ref_kind type member_name ref_property_body
;
property_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| 'readonly' // direct struct members only
| unsafe_modifier // unsafe code support
;
property_body
: '{' accessor_declarations '}' property_initializer?
| '=>' expression ';'
;
property_initializer
: '=' variable_initializer ';'
;
ref_property_body
: '{' ref_get_accessor_declaration '}'
| '=>' 'ref' variable_reference ';'
;
unsafe_modifier (§24.2)仅在不安全的代码(§24)中可用。
property_declaration可能包括一组属性(§23)和任何一种允许的声明辅助功能(§15.3.6)、(§15.3.5)、new(§15.7.2)、 static (§15.6.4) virtual§15.7.6)、 (override、§15.7.6)、 (sealed)、 (abstract、§15.7.6)和(extern)。 此外,struct_declaration直接包含的property_declaration可能包括修饰符readonly(§16.4.11)。
- 第一种会声明一个非引用取值的属性。 它的值的类型是类型。 此类属性可以是可读属性和/或可写属性。
- 第二种会声明一个引用取值的属性。 其值为 variable_reference (§9.5),而它可以是
readonly,也可以是 type 类型的变量。 此类属性仅为可读。
property_declaration可能包括一组属性(§23)和任何一种允许的声明辅助功能(§15.3.6)、(§15.3.5)、new(§15.7.2)、 static (§15.6.4virtual) §15.7.6)、override(§15.6.5、§15.7.6)、 sealed (abstract(§15.6.7、§15.7.6)和extern(§15.6.8)修饰符。
对于修饰符的有效组合,属性声明受方法声明(§15.6)相同的规则约束。
member_name(§15.6.1)指定属性的名称。 除非该属性是显式接口成员实现,否则member_name只是标识符。 对于显式接口成员实现(§19.6.2), member_name 包含 一个interface_type 后跟一个“”.和一个 标识符。
属性 的类型 应至少与属性本身(§7.5.5.5)一样可访问。
property_body可能包括语句正文或表达式正文。 在语句正文中,访问器声明应包含在“{”和“}”标记中,声明属性的访问器(第15.7.3节)。 访问器指定与读取和写入属性关联的可执行语句。
在 property_body 中,由 => 后跟表达式E和分号组成的表达式主体与语句主体 { get { return E; } } 完全等效,因此只能用于指定只读属性,其中 get 访问器的结果由单个表达式给出。
property_initializer 可能只能为自动实现的属性给出 (§15.7.4),并导致使用表达式给出的值来初始化此类属性的底层字段。
ref_property_body可能由语句正文或表达式正文组成。 在语句正文中,get_accessor_declaration 声明了属性的 get 访问器 (§15.7.3)。 访问器指定与读取属性关联的可执行语句。
在 ref_property_body 中,由 => 后跟 ref、variable_referenceV 和分号组成的表达式体与语句主体 { get { return ref V; } } 完全等效。
注意:尽管用于访问属性的语法与字段的语法相同,但不会将属性分类为变量。 因此,无法将属性作为
in、out或ref参数传递,除非该属性为引用取值并因此会返回变量引用 (§9.7)。 尾注
当属性声明包含 extern 修饰符时,该属性据说是外部 属性。 由于外部属性声明不提供实际实现,因此其 accessor_declarations 中的每个 accessor_body 均应为分号。
15.7.2 静态和实例属性
当属性声明包含 static 修饰符时,该属性据说是静态 属性。 当不存在 static 修饰符时,该属性称为 实例属性。
静态属性与特定实例无关联,而在静态属性的访问器中引用 this 属于一种编译时错误。
实例属性与类的某个实例相关联,该实例可以在该属性的访问器中作为this进行§12.8.14访问。
静态成员和实例成员之间的差异在 §15.3.8 中进一步讨论。
15.7.3. 访问器
注意:此子引用适用于属性(§15.7)和索引器(§15.9)。 该小节是依据属性来编写的,当读取索引器时,替换索引器/索引器属性/属性,并参考 §15.9.2 中给出的属性和索引器之间的差异列表。 尾注
属性 accessor_declarations 指定与写入和/或读取该属性关联的可执行语句。
accessor_declarations
: get_accessor_declaration set_accessor_declaration?
| set_accessor_declaration get_accessor_declaration?
;
get_accessor_declaration
: attributes? accessor_modifier? 'get' accessor_body
;
set_accessor_declaration
: attributes? accessor_modifier? 'set' accessor_body
;
accessor_modifier
: 'protected'
| 'internal'
| 'private'
| 'protected' 'internal'
| 'internal' 'protected'
| 'protected' 'private'
| 'private' 'protected'
| 'readonly' // direct struct members only
;
accessor_body
: block
| '=>' expression ';'
| ';'
;
ref_get_accessor_declaration
: attributes? accessor_modifier? 'get' ref_accessor_body
;
ref_accessor_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
accessor_declarations 由 get_accessor_declaration、set_accessor_declaration 或二者组成。 每个访问器声明由可选属性、可选 accessor_modifier、标记 get 或 set,后跟 accessor_body组成。
对于引用取值的属性,ref_get_accessor_declaration 由可选属性、可选 accessor_modifier 和标记 get 组成,且后跟 ref_accessor_body。
accessor_modifier 的使用受以下限制的约束:
-
accessor_modifier
readonly仅允许在直接包含在 struct_declaration 中的 property_declaration 或 indexer_declaration 中使用(§16.4.11、§16.4.13)。 - 对于没有
override修饰符的属性或索引器,只有当属性或索引器同时具有 get 和 set 访问器时,才允许使用 accessor_modifier,并且只能在其中一个访问器上使用。 - 对于包含
override修饰符的属性或索引器,访问器应与被替代的访问器的 accessor_modifier(如有)匹配。 -
accessor_modifier 应声明的可访问性比属性或索引器自身声明的可访问性受到更严格的限制。 确切地说:
- 如果属性或索引器的可访问性已声明为
public,那么由accessor_modifier声明的可访问性可能是private protected、protected internal、internal、protected或private。 - 如果属性或索引器的可访问性已声明为
protected internal,那么由accessor_modifier声明的可访问性可能是private protected、protected private、internal、protected或private。 - 如果属性或索引器具有声明的可访问性为
internal或protected,那么由accessor_modifier声明的可访问性应为private protected或private。 - 如果属性或索引器的声明可访问性是
private protected,那么由accessor_modifier声明的可访问性应该是private。 - 如果属性或索引器具有声明的访问级别
private,则不能使用 accessor_modifier。
- 如果属性或索引器的可访问性已声明为
对于 abstract 与 extern 非引用取值属性,指定的每个访问器的任何 accessor_body 都只是一个分号。 对于指定为分号的所有访问器,非抽象、非外部属性(但不是索引器)也可能具有 accessor_body;在此情况下,它是一种自动实现的属性 (§15.7.4)。 自动实现的属性至少应具有 get 访问器。 对于任何其他非抽象、非外部属性的访问器,accessor_body 为以下二者之一:
- 一个 块 ,指定要在调用相应访问器时执行的语句;或
- 表达式主体由
=>后跟一个表达式和一个分号组成,表示在调用相应访问器时执行的单个表达式。
对于 abstract 与 extern 非引用取值属性,ref_accessor_body 只是一个分号。 对于任何其他非抽象、非外部属性的访问器,ref_accessor_body 为以下二者之一:
- 一个块,它用于指定调用 get 访问器时要执行的语句;或者
- 一个表达式主体,而它由
=>组成,且后跟ref、variable_reference 和一个分号。 调用 get 访问器时,将对变量引用进行评估。
非引用取值属性的 get 访问器对应于返回值为 property 类型的无参数方法。 除赋值的目标外,在表达式中引用此类属性时,将调用 get 访问器来计算属性的值(§12.2.2)。
非 ref 值属性的 get 访问器的主体应符合 §15.6.11 中所述的值返回方法的规则。 具体而言,get 访问器主体中的所有 return 语句应指定可隐式转换为属性类型的表达式。 此外,不应访问 get 访问器的端点。
引用取值属性的 get 访问器对应于返回值为 variable_reference 属性类型的变量的无参数方法。 在表达式中引用此类属性时,将调用 get 访问器来计算 该属性的variable_reference 值。 然后,该 variable reference 与任何其他变量一样,用于读取或对于非只读 variable_reference,并根据上下文的要求写入引用的变量。
示例:以下示例演示 ref 值属性作为赋值的目标:
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 访问器的主体应符合 §15.6.11 中描述的引用取值方法的规则。
set 访问器对应于具有属性类型的单个值参数和 void 返回类型的方法。 set 访问器的隐式参数始终会命名为 value。 当属性被引用为赋值(§12.22)的目标或作数++–- (§12.8.16、§12.9.7)时,将调用 set 访问器并提供新值(§12.22.2)。 set 访问器的主体应符合 void 中描述的 方法的对应规则。 具体而言,不允许 set 访问器主体中的返回语句指定表达式。 由于 set 访问器隐式包含一个名为 `value` 的参数,因此 set 访问器中的局部变量或常量声明不能使用该名称,否则会产生编译时错误。
根据 get 和 set 访问器是否存在,属性分类如下:
- 同时包含 get 访问器和 set 访问器的属性被称为读写属性。
- 仅具有 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 属性的 get 访问器会返回存储在私有string字段中的caption。 set 访问器检查新值是否与当前值不同,如果是这样,它将存储新值并重新修补控件。 属性通常遵循上面所示的模式:get 访问器只返回存储在private字段中的值,而 set 访问器会修改该private字段,然后执行更新对象状态所需的任何其他操作。Button鉴于上述类,下面是使用该Caption属性的示例:Button okButton = new Button(); okButton.Caption = "OK"; // Invokes set accessor string s = okButton.Caption; // Invokes get accessor此处,通过向属性分配值来调用 set 访问器,通过引用表达式中的属性来调用 get 访问器。
结束示例
属性的 get 与 set 访问器不是完全不同的成员,因此不能单独声明属性的访问器。
示例:示例
class A { private string name; // Error, duplicate member name public string Name { get => name; } // Error, duplicate member name public string Name { set => name = value; } }没有声明任何读写属性。 相反,它声明两个具有相同名称的属性,一个只读和一个只写。 由于在同一类中声明的两个成员不能具有相同的名称,该示例会导致发生编译时错误。
结束示例
当派生类按与继承属性相同的名称声明属性时,派生属性将隐藏与读取和写入相关的继承属性。
示例:在以下代码中
class A { public int P { set {...} } } class B : A { public new int P { get {...} } }
P中的属性在读取和写入方面隐藏了B中P的属性。 因此,在语句中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 访问器等同于读取字段的值,因此当 get 访问器具有可观察的副作用时,被认为是不良的编程风格。 在示例中
class Counter { private int next; public int Next => next++; }属性的值
Next取决于以前访问该属性的次数。 因此,访问该属性会产生可观察的副作用,并且该属性应改为作为方法实现。get 访问器的“无副作用”约定并不意味着 get 访问器应该总是简单地编写为仅仅返回存储在字段中的值。 事实上,获取访问器通常通过访问多个字段或调用方法来计算属性的值。 但是,正确设计的 get 访问器不会执行任何导致对象状态可观察更改的操作。
结束示例
属性可用于延迟资源的初始化,直到首次引用资源的那一刻。
示例:
public class Console { private static TextReader reader; private static TextWriter writer; private static TextWriter error; public static TextReader In { get { if (reader == null) { reader = new StreamReader(Console.OpenStandardInput()); } return reader; } } public static TextWriter Out { get { if (writer == null) { writer = new StreamWriter(Console.OpenStandardOutput()); } return writer; } } public static TextWriter Error { get { if (error == null) { error = new StreamWriter(Console.OpenStandardError()); } return error; } } ... }该
Console类包含三个属性,In、Out和Error,分别表示标准输入、输出和错误设备。 通过将这些成员公开为属性,Console类可以延迟其初始化,直到实际使用它们。 例如,在首次引用Out属性时,例如Console.Out.WriteLine("hello, world");为输出设备创建基础
TextWriter。 但是,如果应用程序没有引用In和Error属性,则不会为这些设备创建任何对象。结束示例
15.7.4. 自动实现的属性
自动实现的属性(或简称“自动属性”)是一个非抽象、非外部、非引用取值的属性,且仅具有由分号组成的 accessor_body。 自动属性应具有 get 访问器,且可选择具有 set 访问器。
将属性指定为自动实现的属性时,隐藏后盾字段将自动可用于该属性,并且将实现访问器以读取和写入该后盾字段。 隐藏后盾字段不可访问,它只能通过自动实现的属性访问器读取和写入,即使在包含类型内也是如此。 如果自动属性没有 set 访问器,则支持字段被视为 readonly (§15.5.3)。 与 readonly 字段一样,只读自动属性也可以在封闭类的构造函数中进行赋值。 此类分配将直接分配给属性的只读支持字段。
自动属性可以选择性地具有属性初始化器,其作为变量初始化器直接应用于支持字段(§17.7)。
示例:
public class Point { public int X { get; set; } // Automatically implemented public int Y { get; set; } // Automatically implemented }等效于以下声明:
public class Point { private int x; private int y; public int X { get { return x; } set { x = value; } } public int Y { get { return y; } set { y = value; } } }结束示例
示例:在以下示例中
public class ReadOnlyPoint { public int X { get; } public int Y { get; } public ReadOnlyPoint(int x, int y) { X = x; Y = y; } }等效于以下声明:
public class ReadOnlyPoint { private readonly int __x; private readonly int __y; public int X { get { return __x; } } public int Y { get { return __y; } } public ReadOnlyPoint(int x, int y) { __x = x; __y = y; } }只读字段的赋值有效,因为它们发生在构造函数中。
结束示例
虽然支持字段是隐藏的,但该字段可通过自动实现的属性的 property_declaration 直接应用于该字段 (§15.7.1)。
示例:以下代码
[Serializable] public class Foo { [field: NonSerialized] public string MySecret { get; set; } }导致将面向字段的属性
NonSerialized应用于编译器生成的支持字段,就像代码已按如下方式编写一样:[Serializable] public class Foo { [NonSerialized] private string _mySecretBackingField; public string MySecret { get { return _mySecretBackingField; } set { _mySecretBackingField = value; } } }结束示例
15.7.5 辅助功能
如果访问器具有accessor_modifier,则访问器的可访问性域(§7.5.3)通过使用accessor_modifier的声明的可访问性来确定。 如果访问器没有 accessor_modifier,则访问器的可访问性域取决于属性或索引器的声明可访问性。
accessor_modifier 的存在永远不会影响成员查找(§12.5)或重载解析(§12.6.4)。 无论访问上下文如何,属性或索引器上的修饰符始终确定绑定到哪个属性或索引器。
选择特定的非 ref 值属性或非 ref 值索引器后,使用相关访问器的可访问性域来确定该用法是否有效。
- 如果使用作为一个值(§12.2.2),则必须存在“get”访问器并且能够被访问。
- 如果用法作为简单赋值的目标(§12.22.2),则集访问器应存在并可访问。
- 如果用法是复合赋值的目标(§12.22.4),或者作为或
++运算符的目标--(§12.8.16,§12.9.7),则 get 访问器和 set 访问器应存在并可访问。
示例:在以下示例中,属性
A.Text被属性B.Text隐藏,即使在仅调用 set 访问器的上下文中也是如此。 相比之下,该属性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 } }结束示例
一旦选择了特定的引用值属性或引用值索引器(无论是作为值、简单赋值的目标还是复合赋值的目标使用),所涉及的 get 访问器的可访问性域都用于确定该用法是否有效。
用于实现接口的访问器不应具有 accessor_modifier。 如果只使用一个访问器来实现接口,则其他访问器可以使用accessor_modifier声明:
示例:
public interface I { string Prop { get; } } public class C : I { public string Prop { get => "April"; // Must not have a modifier here internal set {...} // Ok, because I.Prop has no set accessor } }结束示例
15.7.6. 虚拟、封装、替代与抽象访问器
注意:此子引用适用于属性(§15.7)和索引器(§15.9)。 该小节是依据属性来编写的,当读取索引器时,替换索引器/索引器属性/属性,并参考 §15.9.2 中给出的属性和索引器之间的差异列表。 尾注
虚拟属性声明指定属性的访问器是虚拟的。
virtual 修饰符会应用于属性的所有非私有访问器。 当虚拟属性的访问器具有privateaccessor_modifier时,专用访问器隐式不是虚拟的。
抽象属性声明指定属性的访问器是虚拟的,但不提供访问器的实际实现。 相反,非抽象派生类需要通过替代该属性来为访问器提供自己的实现。 由于抽象属性声明的访问器不提供实际实现,因此其 accessor_body 只包含分号。 抽象属性不应具有 private 访问器。
同时包含 abstract 与 override 修饰符的属性声明指定该属性是抽象的,并会替代基属性。 此类属性的访问器也是抽象的。
抽象属性声明仅在抽象类(§15.2.2.2)和接口(§19.4.4)中得到允许。 通过包含指定 override 指令的属性声明,可在派生类中替代继承的虚拟属性的访问器。 这称为 重写属性声明。 替代属性声明不会声明新属性。 相反,它只是专门化现有虚拟属性的访问器的实现。
替代声明和重写的基属性需要具有相同的声明的可访问性。 换句话说,重写声明不应更改基属性的可访问性。 但是,如果重写的基属性是受保护的内部属性,并且它是在与包含重写声明的程序集不同的程序集内声明的,那么重写声明的声明可访问性应为受保护。 如果继承的属性只有一个访问器(即,如果继承的属性是只读的或只写的),则重写属性应仅包含该访问器。 如果继承的属性包括两个访问器(即,如果继承的属性是读写的),则重写属性可以包括单个访问器或两个访问器。 替代的类型与继承的属性之间应存在标识转换。
替代属性声明可能包括 sealed 修饰符。 使用此修饰符可防止派生类进一步替代该属性。 封装属性的访问器也是封装的。
除了声明和调用语法的差异外,虚拟、密封、重写和抽象访问器的行为与虚拟、密封、重写和抽象方法完全相同。 具体而言,§15.6.4、§15.6.5、§15.6.6 和 §15.6.7 中所述的规则适用,就像访问器是相应表单的方法一样:
- get 访问器对应于无参数方法,其返回值为 property 类型,且具有与包含属性相同的修饰符。
- set 访问器对应于具有 property 类型的单个值参数、无效返回类型以及与包含属性相同的修饰符的方法。
示例:在以下代码中
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的 get 访问器和Y的 set 访问器使用基关键字来访问继承的访问器。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_declaration 进行声明:
event_declaration
: attributes? event_modifier* 'event' type variable_declarators ';'
| attributes? event_modifier* 'event' type member_name
'{' event_accessor_declarations '}'
;
event_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| 'readonly' // direct struct members only
| unsafe_modifier // unsafe code support
;
event_accessor_declarations
: add_accessor_declaration remove_accessor_declaration
| remove_accessor_declaration add_accessor_declaration
;
add_accessor_declaration
: attributes? 'add' block
;
remove_accessor_declaration
: attributes? 'remove' block
;
unsafe_modifier (§24.2)仅在不安全的代码(§24)中可用。
event_declaration可能包括一组属性(§23)和任何一种允许的声明辅助功能(§15.3.6)、(15.3.5)、new (static、§15.8.4)、(§15.6.4、virtual§15.8.5)、 override (§15.6.5、§15.8.5)、 sealed (§15.6.6)、abstract(§15.6.7、§15.8.5)和extern(§15.6.8)修饰符。 此外,直接由struct_declaration包含的event_declaration可以包括readonly修饰符(§16.4.12)。
对于修饰符的有效组合,事件声明受方法声明(§15.6)相同的规则约束。
事件声明的类型应为delegate_type(§8.2.8),并且该delegate_type至少应与事件本身(§7.5.5.5)一样可访问。
事件声明可包含 event_accessor_declaration。 但是,如果不这样做,对于非外部的非抽象事件,编译器将自动提供这些事件(§15.8.2);而对于 extern 事件,访问器由外部提供。
省略 event_accessor_declaration 的事件声明定义一个或多个事件 — 每个事件对应一个 variable_declarator。 属性和修饰符会应用于此类 event_declaration 声明的所有成员。
event_declaration 同时包含 abstract 修饰符和 event_accessor_declaration 属于一种编译时错误。
当事件声明包含 extern 修饰符时,事件据说是外部 事件。 由于外部事件声明不提供实际实现,因此如果同时包含 extern 修饰符和 event_accessor_declaration,则这是错误的。
对于带有 或 abstract 修饰符的事件声明的 external 包含 variable_initializer 属于一种编译时错误。
事件可以作为+=和-=运算符的左操作数来使用。 这些运算符分别用于将事件处理程序附加到或从事件中删除事件处理程序,以及事件的访问修饰符控制允许此类操作的上下文。
由声明该事件的类型之外的代码允许对事件执行的唯一操作是 += 和 -=。 因此,尽管此类代码可以添加和删除事件的处理程序,但它无法直接获取或修改事件处理程序的基础列表。
在窗体x += y的作中,或者x –= y,如果x某个事件,则作的结果的类型void为 (§12.22.5) (而不是具有xx赋值后的值,对于非事件类型定义的其他+=和-=运算符)。 这样可防止外部代码间接检查事件的基础委托。
示例:以下示例演示如何将事件处理程序附加到类的
Button实例:public delegate void EventHandler(object sender, EventArgs e); public class Button : Control { public event EventHandler Click; } public class LoginDialog : Form { Button okButton; Button cancelButton; public LoginDialog() { okButton = new Button(...); okButton.Click += new EventHandler(OkButtonClick); cancelButton = new Button(...); cancelButton.Click += new EventHandler(CancelButtonClick); } void OkButtonClick(object sender, EventArgs e) { // Handle okButton.Click event } void CancelButtonClick(object sender, EventArgs e) { // Handle cancelButton.Click event } }在这里,
LoginDialog实例构造函数创建两Button个实例,并将事件处理程序附加到Click事件。结束示例
15.8.2. 类似字段的事件
在包含事件声明的类或结构的程序文本中,某些事件可以像字段一样使用。 若要以这种方式使用,事件不得是抽象或外部,并且不得显式地包含 event_accessor_declaration。 此类事件可以在允许字段的任何上下文中使用。 该字段包含一个委托(§21),该委托引用已添加到事件的事件处理程序列表。 如果未添加事件处理程序,则字段包含 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与+=运算符左侧的–=的引用会导致调用 add 与 remove 访问器。 所有其他引用Ev的地方都被编译为转而引用隐藏字段__Ev(§12.8.7)。 名称“__Ev”是任意的;隐藏字段可能具有任何名称或根本没有名称。尾注
15.8.3 事件访问器
注意:事件声明通常省略 event_accessor_declarations,如上面的示例所示
Button。 例如,如果每个事件一个字段的存储成本不可接受,则可能会包括这些字段。 在这种情况下,类可以包含 event_accessor_declarations,并使用专用机制来存储事件处理程序列表。 尾注
事件的 event_accessor_declarations 指定与添加和删除事件处理程序关联的可执行语句。
访问器声明由add_accessor_declaration和remove_accessor_declaration组成。 每个访问器声明都由标记 add 或 remove 后跟一个块组成。 与add_accessor_declaration关联的块指定要在添加事件处理程序时执行的语句,并且与remove_accessor_declaration关联的块指定要在删除事件处理程序时执行的语句。
每个 add_accessor_declaration 和 remove_accessor_declaration 对应于带有事件类型单个值参数和 void 返回类型的方法。 事件访问器的隐式参数命名 value。 在事件分配中使用事件时,将使用相应的事件访问器。 具体而言,如果赋值运算符是 +=,则使用 add 访问器;如果赋值运算符是 –=,则使用 remove 访问器。 在任一情况下,赋值运算符的右操作数用作事件访问器的参数。
add_accessor_declaration或remove_accessor_declaration的块应符合void§15.6.9中所描述的方法规则。 具体而言, return 不允许此类块中的语句指定表达式。
由于事件访问器隐式具有一个名为value的参数,因此在事件访问器中声明的本地变量或常量具有该名称时,会导致编译时错误。
示例:在以下代码中
class Control : Component { // Unique keys for events static readonly object mouseDownEventKey = new object(); static readonly object mouseUpEventKey = new object(); // Return event handler associated with key protected Delegate GetEventHandler(object key) {...} // Add event handler associated with key protected void AddEventHandler(object key, Delegate handler) {...} // Remove event handler associated with key protected void RemoveEventHandler(object key, Delegate handler) {...} // MouseDown event public event MouseEventHandler MouseDown { add { AddEventHandler(mouseDownEventKey, value); } remove { RemoveEventHandler(mouseDownEventKey, value); } } // MouseUp event public event MouseEventHandler MouseUp { add { AddEventHandler(mouseUpEventKey, value); } remove { RemoveEventHandler(mouseUpEventKey, value); } } // Invoke the MouseUp event protected void OnMouseUp(MouseEventArgs args) { MouseEventHandler handler; handler = (MouseEventHandler)GetEventHandler(mouseUpEventKey); if (handler != null) { handler(this, args); } } }该
Control类实现事件的内部存储机制。 该方法AddEventHandler将委托值与键相关联,GetEventHandler该方法返回当前与键关联的委托,该方法RemoveEventHandler将删除委托作为指定事件的事件处理程序。 大概,基础存储机制的设计是将 null 委托值与键关联起来时不产生存储空间的消耗,因此,未处理的事件不占用存储空间。结束示例
15.8.4 静态事件和实例事件
当事件声明包含 static 修饰符时,事件据说是静态 事件。 当不存在 static 修饰符时,事件将称为实例 事件。
静态事件不与特定实例关联,在静态事件的访问器中引用 this 会导致编译时错误。
实例事件与类的给定实例相关联,此实例可在该事件的访问器中作为this进行访问(§12.8.14)。
静态成员和实例成员之间的差异在 §15.3.8 中进一步讨论。
15.8.5. 虚拟、封装、替代与抽象访问器
虚拟事件声明指定该事件的访问器是虚拟的。 修饰符 virtual 适用于事件的两个访问器。
抽象事件声明指定事件的访问器是虚拟的,但不提供访问器的实际实现。 相反,非抽象派生类需要通过替代该事件来为访问器提供自己的实现。 由于抽象事件声明的访问器不提供实际实现,因此它不应提供 event_accessor_declaration。
包含 abstract 和 override 修饰符的事件声明指定事件是抽象的,并重写基事件。 此类事件的访问器也是抽象的。
抽象事件声明仅在抽象类(§15.2.2.2)和接口(§19.4.5)中得到允许。
通过包含指定 override 修饰符的事件声明,可在派生类中替代继承的虚拟事件的访问器。 这称为 重定义事件声明。 替代事件声明不会声明新事件。 相反,它只是对现有虚拟事件的访问器的实现进行专门化。
重写事件声明应指定与被重写事件完全相同的访问修饰符和名称,重写事件的类型与被重写事件的类型之间应存在类型转换,并且在声明中应指定添加和删除访问器。
重载事件声明可以包含 sealed 修饰符。 使用 this 修饰符可防止派生类进一步重写事件。 封装事件的访问器也是封装的。
替代事件声明包含 new修饰符属于一种编译时错误。
除了声明和调用语法的差异外,虚拟、密封、重写和抽象访问器的行为与虚拟、密封、重写和抽象方法完全相同。 具体而言,§15.6.4、§15.6.5、§15.6.6 和 §15.6.7 中所述的规则适用,就像访问器是相应表单的方法一样。 每个访问器对应于一个方法,该方法具有事件类型的单个值参数、 void 返回类型,以及与包含事件相同的修饰符。
15.9 索引器
15.9.1 常规
索引器是一个成员,它使对象能够以与数组相同的方式编制索引。 索引器通过 indexer_declaration 声明而定义:
indexer_declaration
: attributes? indexer_modifier* indexer_declarator indexer_body
| attributes? indexer_modifier* ref_kind indexer_declarator ref_indexer_body
;
indexer_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| 'readonly' // direct struct members only
| unsafe_modifier // unsafe code support
;
indexer_declarator
: type 'this' '[' parameter_list ']'
| type interface_type '.' 'this' '[' parameter_list ']'
;
indexer_body
: '{' accessor_declarations '}'
| '=>' expression ';'
;
ref_indexer_body
: '{' ref_get_accessor_declaration '}'
| '=>' 'ref' variable_reference ';'
;
unsafe_modifier (§24.2)仅在不安全的代码(§24)中可用。
indexer_declaration可能包括一组属性(§23)和任何一种允许的声明辅助功能(§15.3.6)、(§15.3.5)、new(§15.3.5)、 virtual (§15) .6.4、 override (§15.6.5)、 sealed (§15.6.6.6)、 abstract (§15.6.7)和extern(§15.6.8)修饰符。 此外,直接包含在struct_declaration中的indexer_declaration可能包括readonly修饰符(§16.4.12)。
- 第一种会声明一个非引用取值的索引器。 它的值的类型是类型。 此类索引器可以是可读的和/或可写的。
- 第二种会声明一个引用取值的索引器。 其值为 variable_reference (§9.5),而它可以是
readonly,也可以是 type 类型的变量。 此类索引器仅为可读。
indexer_declaration可能包括一组属性(§23)和任何一种允许的声明辅助功能(§15.3.6)、(§15.3.5)、new(§15.3.5)、 virtual (§15) .6.4、 override (§15.6.5)、sealed(§15.6.6.6)、 abstract (§15.6.7)和extern(§15.6.8)修饰符。
对于修饰符的有效组合,索引器声明受方法声明(§15.6)相同的规则,但有一个例外是 static 不允许对索引器声明使用修饰符。
索引器声明的类型指定声明引入的索引器的元素类型。
注意:由于索引器旨在用于数组类似元素的上下文,因此为数组定义的术语 元素类型 也与索引器一起使用。 尾注
除非索引器是显式接口成员的实现,否则关键字应该位于this之后。 对于显式接口成员实现,type 会后跟 interface_type、一个“.”和关键字 this。 与其他成员不同,索引器没有用户定义的名称。
parameter_list指定索引器的参数。 索引器的参数列表对应于方法(§15.6.2)的参数列表,但至少应指定一个参数,并且不允许使用this、ref和out参数修饰符。
索引器的类型和parameter_list中引用的每个类型至少应与索引器本身(§7.5.5.5)一样可访问。
indexer_body可能包括语句正文(§15.7.1)或表达式正文(§15.6.1)。 在语句正文中, accessor_declarations(应包含在“{”和“”}标记中)中,声明索引器的访问器(§15.7.3)。 访问器指定与读取和写入索引器元素关联的可执行语句。
在 indexer_body 中,由 “=>” 后跟表达式 E 和分号组成的表达式主体与语句主体 { get { return E; } } 完全等效,因此只能用于指定只读索引器,其中 get 访问器的结果由单个表达式给出。
ref_indexer_body可能包含语句正文或表达式正文。 在语句正文中,get_accessor_declaration 声明了索引器的 get 访问器 (§15.7.3)。 访问器指定与读取索引器关联的可执行语句。
在 ref_indexer_body 中,由 => 后跟 ref、variable_referenceV 和分号组成的表达式体与语句主体 { get { return ref V; } } 完全等效。
注意:即使访问索引器元素的语法与数组元素的语法相同,索引器元素也不会归类为变量。 因此,无法将索引器元素作为
in、out或ref参数传递,除非该索引器为引用取值并因此会返回变量引用 (§9.7)。 尾注
索引器的参数列表定义了索引器的签名(§7.6)。 具体而言,索引器的签名包含其参数的数量和类型。 参数的元素类型和名称不是索引器的签名的一部分。
索引器的签名应不同于在同一类中声明的所有其他索引器的签名。
索引器声明包含extern修饰符时,索引器据说是外部索引器。 由于外部索引器声明不提供实际实现,因此其 accessor_declarations 中的每个 accessor_body 均应为分号。
示例:以下示例声明一个
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和经典的“sieve”算法来计算 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.4)进行访问。
- 属性可以是静态成员,而索引器始终是实例成员。
- 属性的 get 访问器对应于没有参数的方法,而索引器的 get 访问器对应于与索引器具有相同参数列表的方法。
- 属性的 set 访问器对应于具有名为
value的单个参数的方法,而索引器的 set 访问器对应于与索引器具有相同参数列表的方法,外加名为value的附加参数。 - 索引器访问器声明与索引器参数同名的局部变量或本地常量是编译时错误。
- 在重写属性声明中,使用语法
base.P来访问继承的属性,其中P是属性名称。 在重写索引器声明中,使用语法base[E]访问继承的索引器,其中E是逗号分隔的表达式列表。 - 没有“自动实现的索引器”的概念。 使用分号 accessor_body 的非抽象、非外部索引器是错误的。
除了这些差异之外,§15.7.3、§15.7.5 和 §15.7.6 中定义的所有规则也适用于索引器访问器和属性访问器。
在读取 §15.7.3、§15.7.5 和 §15.7.6 时,将属性替换为索引器也会应用于定义的术语。 具体而言,读写属性变为读写索引器,只读属性成为只读索引器,写入属性变为写入索引器。
15.10 运算符
15.10.1 总体
运算符是一个成员,用于定义可应用于类实例的表达式运算符的含义。 运算符使用 operator_declarations 声明:
operator_declaration
: attributes? operator_modifier+ operator_declarator operator_body
;
operator_modifier
: 'public'
| 'static'
| 'extern'
| unsafe_modifier // unsafe code support
;
operator_declarator
: unary_operator_declarator
| binary_operator_declarator
| conversion_operator_declarator
;
unary_operator_declarator
: type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
;
logical_negation_operator
: '!'
;
overloadable_unary_operator
: '+' | '-' | logical_negation_operator | '~' | '++' | '--' | 'true' | 'false'
;
binary_operator_declarator
: type 'operator' overloadable_binary_operator
'(' fixed_parameter ',' fixed_parameter ')'
;
overloadable_binary_operator
: '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^' | '<<'
| right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
;
conversion_operator_declarator
: 'implicit' 'operator' type '(' fixed_parameter ')'
| 'explicit' 'operator' type '(' fixed_parameter ')'
;
operator_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§24.2)仅在不安全的代码(§24)中可用。
注意:前缀逻辑否定(§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 为代码块体或表达式体。
以下规则适用于所有运算符声明:
- 运算符声明应同时包含 a
public和static修饰符。 - 运算符的参数不应具有其他
in修饰符。 - 运算符 (§15.10.2, §15.10.3, §15.10.4) 的签名应不同于同一类中声明的所有其他运算符的签名。
- 运算符声明中引用的所有类型至少应与运算符本身(§7.5.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.7) 一样。 与C++不同,此方法不应直接修改其操作数的值,因为这将违反后缀递增运算符(§12.8.16)的标准语义。
结束示例
15.10.3 二进制运算符
以下规则适用于二进制运算符声明,其中 T 表示包含运算符声明的类或结构的实例类型:
- 二进制非移位运算符应采用两个参数,其中至少一个参数应具有类型
T,T?也可以返回任何类型。 - 二进制
<<或>>运算符(§12.12)应采用两个参数,其中第一个参数应具有类型,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。 仅当以下所有内容均为 true 时,才允许类或结构声明从源类型 S 转换为目标类型 T :
S₀和T₀不同类型的。S₀或者T₀是包含运算符声明的类或结构的实例类型。S₀和T₀都不是接口类型。排除用户定义的转换,不存在从
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的转换。 结束示例
无法直接重新定义预定义的转换。 因此,不允许转换运算符从 1 转换或转换为 object,因为 object 与所有其他类型的之间已经存在隐式和显式转换。 同样,转化的源类型和目标类型都不能是另一种转化的基本类型,因为这样转化就已经存在了。 但是,可以在泛型类型上声明运算符,这些运算符可以针对特定类型参数指定已经存在的预定义转换。
示例:
struct Convertible<T> { public static implicit operator Convertible<T>(T value) {...} public static explicit operator T(Convertible<T> value) {...} }当类型
object指定为T的类型参数时,第二个运算符声明已存在的转换(从任何类型到类型对象的隐式转换,因此也是显式转换)。结束示例
如果两种类型之间存在预定义转换,则忽略这些类型之间的任何用户定义的转换。 具体而言:
- 如果从类型到
S类型存在预定义的隐式转换(§10.2),则所有从T到S的用户定义转换(无论是隐式或显式)都会被忽略。 - 如果存在从类型 到类型
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 时不会发生用户定义的转换,并且仅当正在转换的 实际实现指定的 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 (§24.2)仅在不安全的代码(§24)中可用。
constructor_declaration可以包括一组属性(§23)、任何一种允许的声明辅助功能(§15.3.6)和一个extern(§15.6.8)修饰符。 不允许构造函数声明多次包含相同的修饰符。
constructor_declarator的标识符应命名声明实例构造函数的类。 如果指定了任何其他名称,则会发生编译时错误。
实例构造函数的可选parameter_list遵循与方法parameter_list相同的规则(§15.6)。 由于参数的 this 修饰符仅适用于扩展方法(§15.6.10),因此构造函数 的parameter_list 中没有任何参数应包含 this 修饰符。 参数列表定义实例构造函数的签名(§7.6),并控制重载解析(§12.6.4)在调用中选择特定实例构造函数的过程。
实例构造函数parameter_list中引用的每个类型至少应与构造函数本身(§7.5.5.5)一样可访问。
可选constructor_initializer指定要调用的另一个实例构造函数,然后再执行此实例构造函数的constructor_body中指定的语句。 这在 §15.11.2 中进一步介绍。
当构造函数声明包含extern修饰符时,构造函数称为外部构造函数。 由于外部构造函数声明不提供实际实现,因此其 constructor_body 由分号组成。 对于所有其他构造函数,constructor_body 由
- 一个块,用于指定初始化类新实例的语句;或
- 表达式正文由
=>、一个 表达式 和分号组成,表示用于初始化类的新实例的单个表达式。
constructor_body 即一个块或表达式体,与返回类型 的实例方法的void完全对应 (§15.6.11)。
实例构造函数不能被继承。 因此,类没有在类中实际声明的构造函数以外的实例构造函数,但例外是,如果类不包含实例构造函数声明,则会自动提供默认实例构造函数(§15.11.5)。
实例构造函数是由 object_creation_expression(§12.8.17.2)和 constructor_initializer 调用的。
15.11.2. 构造函数初始值设定项
所有实例构造函数(除了object类)都会在构造函数主体之前隐式调用另一个实例构造函数。 要隐式调用的构造函数由 constructor_initializer 确定:
-
base(argument_list)形式的实例构造函数初始值设定项(其中 argument_list 是可选的)会导致调用直接基类中的实例构造函数。 使用argument_list和 §12.6.4 的重载解析规则选择该构造函数。 候选实例构造函数集由直接基类的所有可访问实例构造函数组成。 如果此集为空,或者无法识别单个最佳实例构造函数,则会发生编译时错误。 -
this(argument_list)形式的实例构造函数初始值设定项(其中 argument_list 是可选的)会调用同一类中的另一实例构造函数。 使用argument_list和 §12.6.4 的重载解析规则选择构造函数。 候选实例构造函数集包含类本身中声明的所有实例构造函数。 如果生成的一组适用的实例构造函数为空,或者无法识别单个最佳实例构造函数,则会发生编译时错误。 如果实例构造函数声明通过一个或多个构造函数初始值设定项链调用自身,则会发生编译时错误。
如果实例构造函数没有构造函数初始化器,则会隐式提供形式为 base() 的构造函数初始化器。
注意:因此,以下形式的实例构造函数声明
C(...) {...}完全等效于
C(...) : base() {...}尾注
实例构造函数声明中的parameter_list提供的参数范围包括该声明的构造函数初始值设定项。 因此,允许构造函数初始值设定项访问构造函数的参数。
示例:
class A { public A(int x, int y) {} } class B: A { public B(int x, int y) : base(x + y, x - y) {} }结束示例
实例构造函数初始值设定项无法访问正在创建的实例。 因此,在构造函数初始化表达式的参数中引用 `this` 是编译时错误,因为使用 simple_name 引用任何实例成员的参数表达式也是编译时错误。
15.11.3. 实例变量初始值设定项
当非外部实例构造函数没有构造函数初始值设定项时,或其构造函数初始值设定项形式为 base(...) 时,该构造函数将隐式执行由类中声明的实例字段的 variable_initializer 指定的初始化操作。 这对应于在进入构造函数后和隐式调用直接基类构造函数之前立即执行的一系列赋值。 变量初始值设定项按照类声明(§15.5.6)中显示的文本顺序执行。
不需要由外部实例构造函数执行变量初始值设定项。
15.11.4. 构造函数执行
变量初始值设定项将转换为赋值语句,并在调用基类实例构造函数之前执行这些赋值语句。 此排序可确保在执行任何有权访问该实例的语句之前,所有实例字段通过其变量初始化器进行初始化。
示例:给定以下内容:
class A { public A() { PrintFields(); } public virtual void PrintFields() {} } class B: A { int x = 1; int y; public B() { y = -1; } public override void PrintFields() => Console.WriteLine($"x = {x}, y = {y}"); }当使用新
B()项创建实例B时,将生成以下输出:x = 1, y = 0值为 1,因为变量初始值
x设定项是在调用基类实例构造函数之前执行的。 但是,y的值为 0(int的默认值),因为直到基类构造函数返回后,y的赋值才会执行。 将实例变量初始值设定项和构造函数初始值设定项视为在constructor_body之前自动插入的语句非常有用。 示例class A { int x = 1, y = -1, count; public A() { count = 0; } public A(int n) { count = n; } } class B : A { double sqrt2 = Math.Sqrt(2.0); ArrayList items = new ArrayList(100); int max; public B(): this(100) { items.Add("default"); } public B(int n) : base(n - 1) { max = n; } }包含多个变量初始值设定项;它还包含两种形式(
base和this)的构造函数初始值设定项。 该示例对应于下面所示的代码,其中每个注释指示自动插入的语句(用于自动插入的构造函数调用的语法无效,但仅用于说明机制)。class A { int x, y, count; public A() { x = 1; // Variable initializer y = -1; // Variable initializer object(); // Invoke object() constructor count = 0; } public A(int n) { x = 1; // Variable initializer y = -1; // Variable initializer object(); // Invoke object() constructor count = n; } } class B : A { double sqrt2; ArrayList items; int max; public B() : this(100) { B(100); // Invoke B(int) constructor items.Add("default"); } public B(int n) : base(n - 1) { sqrt2 = Math.Sqrt(2.0); // Variable initializer items = new ArrayList(100); // Variable initializer A(n - 1); // Invoke A(int) constructor max = n; } }结束示例
15.11.5 默认构造函数
如果类不包含实例构造函数声明,则会自动提供默认实例构造函数。 该默认构造函数只是调用直接基类的构造函数,就像它具有形式为 base() 的构造函数初始化器一样。 如果类是抽象的,则默认构造函数的声明访问级别是受保护的。 否则,默认构造函数的声明的可访问性为公开。
注意:因此,默认构造函数始终为某种形式
protected C(): base() {}或
public C(): base() {}其中
C是类的名称。尾注
如果重载决策无法确定基类构造函数初始值设定项的唯一最佳候选项,则会发生编译时错误。
示例:在以下代码中
class Message { object sender; string text; }提供了默认构造函数,因为该类不包含实例构造函数声明。 因此,该示例正好等同于
class Message { object sender; string text; public Message() : base() {} }结束示例
15.12 静态构造函数
静态构造函数是实现初始化封闭类所需的操作的成员。 静态构造函数使用 static_constructor_declarations 声明:
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 (§24.2)仅在不安全的代码(§24)中可用。
static_constructor_declaration可能包括一组属性(§23)和extern修饰符(§15.6.8)。
static_constructor_declaration的标识符应命名声明静态构造函数的类。 如果指定了任何其他名称,则会发生编译时错误。
当静态构造函数声明包含 extern 修饰符时,静态构造函数据说是 外部静态构造函数。 由于外部静态构造函数声明不提供实际实现,因此其 static_constructor_body 由分号组成。 对于所有其他静态构造函数声明, static_constructor_body 由任一类型组成
- 一个 块,它指定要执行的语句以初始化类;或
- 一个表达式主体,它由
=>后跟表达式和一个分号组成,表示要执行的单个表达式,以便初始化类。
static_constructor_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 (§24.2)仅在不安全的代码(§24)中可用。
finalizer_declaration可能包括一组属性(§23)。
finalizer_declarator的标识符应命名声明终结器所在的类。 如果指定了任何其他名称,则会发生编译时错误。
当终结器声明包含extern修饰符时,终结器据说是外部终结器。 由于外部终结器声明不提供实际实现,因此其 finalizer_body 由分号组成。 对于所有其他终结器,finalizer_body 由
- 一个 块,它指定要执行的语句,以便完成类的实例。
- 或表达式主体,它由
=>和一个表达式以及一个分号组成,表示要执行的单个表达式,以完成类的实例化。
finalizer_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方法。结束示例
有关从终结器引发异常时的行为的讨论,请参阅 §22.4。
15.14 异步函数
15.14.1 常规
使用修饰符的方法(§15.6)、匿名函数(§12.20)或本地函数(async)称为异步函数。 一般情况下,术语 异步 用于描述具有 async 修饰符的任何类型的函数。
异步函数的参数列表如果指定任何in、out、ref参数或任何ref struct类型的参数,则会产生编译时错误。
异步方法return_type应为void任务类型或异步迭代器类型(§15.15)。 对于生成结果值的异步方法,任务类型或异步迭代器类型(§15.15.3)应为泛型。 对于不生成结果值的异步方法,任务类型不应为泛型。 此类类型在此规范中分别称为«TaskType»<T>和«TaskType»。 标准库类型System.Threading.Tasks.Task和从System.Threading.Tasks.Task<TResult>、System.Threading.Tasks.ValueTask<T>构造的类型是任务类型,还有通过属性与相关联的类、结构或接口类型。 此类类型在此规范中称为«TaskBuilderType»<T>和«TaskBuilderType»。 任务类型最多可以有一个类型参数,不能嵌套在泛型类型中。
返回任务类型的异步方法据说是任务返回。
任务类型在确切定义中可能有所不同,但从语言的角度来看,任务类型处于不完整、成功或出错的状态之一。
出现故障任务会记录相关异常。
已成功«TaskType»<T>会记录 T 类型的结果。 任务类型是可等待的,因此任务可以是 await 表达式(§12.9.9)的作数。
示例:任务类型
MyTask<T>与任务生成器类型和MyTaskMethodBuilder<T>awaiter 类型Awaiter<T>相关联:using System.Runtime.CompilerServices; [AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))] class MyTask<T> { public Awaiter<T> GetAwaiter() { ... } } class Awaiter<T> : INotifyCompletion { public void OnCompleted(Action completion) { ... } public bool IsCompleted { get; } public T GetResult() { ... } }结束示例
任务生成器类型是对应于特定任务类型的类或结构类型(§15.14.2)。 任务生成器类型应与其相应任务类型的声明可访问性完全匹配。
注意: 如果声明
internal任务类型,还必须在同一程序集中声明internal并定义相应的生成器类型。 如果任务类型嵌套在另一个类型内,则任务生成器类型也必须嵌套在同一类型中。 尾注
异步函数可以通过 await 表达式(§12.9.9)在其正文中暂停计算。 稍后可以通过恢复代理在暂停 await 表达式时恢复评估。 恢复代理的类型为 System.Action,调用时,异步函数调用的计算将从 await 表达式中断的位置恢复。 如果函数调用从未暂停,异步函数调用的当前调用方则为原始调用方,否则为恢复代理的最新调用方。
15.14.2 任务类型生成器模式
任务生成器类型最多可以有一个类型参数,不能嵌套在泛型类型中。 任务生成器类型应具有以下成员(对于非泛型任务生成器类型, SetResult 没有参数),具有声明 public 的可访问性:
class «TaskBuilderType»<T>
{
public static «TaskBuilderType»<T> Create();
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine;
public void SetStateMachine(IAsyncStateMachine stateMachine);
public void SetException(Exception exception);
public void SetResult(T result);
public void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine;
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine;
public «TaskType»<T> Task { get; }
}
编译器应生成使用 «TaskBuilderType» 实现暂停和恢复异步函数计算的语义的代码。 编译器按如下方式使用 «TaskBuilderType»:
-
«TaskBuilderType».Create()调用 以创建在此列表中命名builder的 «TaskBuilderType» 实例。 -
builder.Start(ref stateMachine)调用 将生成器与编译器生成的状态机实例stateMachine相关联。- 构建器应在
stateMachine.MoveNext()中调用Start()或在Start()返回后以推进状态机。
- 构建器应在
-
Start()返回后,async方法会调用builder.Task以使任务从 async 方法返回。 - 每次调用
stateMachine.MoveNext()都会使状态机前进。 - 如果状态机成功完成,则调用
builder.SetResult(),并传递方法的返回值(如果有)。 - 否则,如果在状态机中抛出异常
e,则调用builder.SetException(e)。 - 如果状态机达到
await expr表达式时,则调用expr.GetAwaiter()。 - 如果 awaiter 实现
ICriticalNotifyCompletion且IsCompleted为 false,状态机将builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)调用 。- awaiter 完成时,
AwaitUnsafeOnCompleted()应调用awaiter.UnsafeOnCompleted(action),并附带用于调用Action的stateMachine.MoveNext()。
- awaiter 完成时,
- 否则,状态机会调用
builder.AwaitOnCompleted(ref awaiter, ref stateMachine)。- awaiter 完成时,
AwaitOnCompleted()应调用awaiter.OnCompleted(action),并附带用于调用Action的stateMachine.MoveNext()。
- awaiter 完成时,
-
SetStateMachine(IAsyncStateMachine)编译器生成的IAsyncStateMachine实现可以调用以标识与状态机实例关联的生成器实例,尤其是对于将状态机实现为值类型的情况。- 如果生成器调用
stateMachine.SetStateMachine(stateMachine),则stateMachine将对与builder.SetStateMachine(stateMachine)相关联的生成器实例调用stateMachine。
- 如果生成器调用
注意:对于 和
SetResult(T result)两者来说,参数和实参分别必须能够身份转换为«TaskType»<T> Task { get; }。 这允许任务类型生成器支持元组等类型,其中两个不相同的类型是可转换的。 尾注
15.14.3. 返回任务的异步函数的求值
调用任务返回异步函数会导致生成返回的任务类型的实例。 这称为 异步函数的返回任务 。 任务最初处于 不完整 状态。
然后,对异步函数主体进行求值,直到它被挂起(通过到达 await 表达式)或终止,此时控制权将与返回任务一起返回给调用方。
当 async 函数的主体终止时,返回任务将退出非完成状态:
- 如果函数体由于到达返回语句或函数体末尾而终止,任何结果值将记录在返回任务中,该任务将被标记为成功状态。
- 如果函数体因未捕获的
OperationCanceledException而终止,则异常会记录在返回的任务中,该任务会被置于取消状态。 - 如果函数体由于任何其他未捕获的异常 (§13.10.6) 而终止,则该异常将记录在返回任务中,该任务将进入出现故障状态。
15.14.4. 无效返回的异步函数的求值
如果异步函数 void的返回类型为,则计算方式与上述方法不同:由于没有返回任何任务,因此该函数将完成情况和异常传达给当前线程的 同步上下文。 同步上下文的确切定义依赖于实现,表示当前线程的运行位置。 当 void 返回的异步函数的求值开始、成功完成或导致引发未捕获的异常时,将通知同步上下文。
这样,上下文就可以跟踪其下返回void的异步函数有多少,并决定如何传递从中产生的异常。
15.15 同步迭代器和异步迭代器
15.15.1 常规
使用迭代器块(§13.3)实现的函数成员(§12.6)或本地函数(§13.6.4)称为迭代器。 只要相应函数的返回类型是枚举器接口(§15.15.2)或可枚举接口之一(§15.15.3),迭代器块就可以用作函数的主体。
异步函数(§15.14)或本地函数(§13.6.4)使用迭代器块(§13.3)实现,称为 异步迭代器。 只要相应函数的返回类型是异步枚举器接口(§15.15.2)或异步可枚举接口(§15.15.3),异步迭代器块就可以用作函数的主体。
迭代器块可能作为 method_body、 operator_body 或 accessor_body发生,而事件、实例构造函数、静态构造函数和终结器不应作为同步迭代器或异步迭代器实现。
使用迭代器块实现函数时,对于函数的参数列表,指定类型的任何inout参数或ref参数ref struct是编译时错误。
异步迭代器应支持取消异步作。 这在 §23.5.8 中介绍。
15.15.2 枚举器接口
枚举器接口是非泛型接口System.Collections.IEnumerator和泛型接口System.Collections.Generic.IEnumerator<T>。
异步枚举器接口是泛型接口System.Collections.Generic.IAsyncEnumerator<T>。
为了简洁起见,在此子集合及其同级中,这些接口分别被引用为 IEnumerator、 IEnumerator<T>和 IAsyncEnumerator<T>。
15.15.3 可枚举接口
可枚举接口是非泛型接口System.Collections.IEnumerable和泛型接口System.Collections.Generic.IEnumerable<T>。
异步可枚举接口是泛型接口System.Collections.Generic.IAsyncEnumerable<T>。
为了简洁起见,在此子集合及其同级中,这些接口分别被引用为 IEnumerable、 IEnumerable<T>和 IAsyncEnumerable<T>。
15.15.4. Yield 类型
迭代器生成一系列值(所有相同类型)。 此类型称为 迭代器的生成类型 。
- 迭代器返回
IEnumerator或IEnumerable时,其生成类型为object。 - 迭代器返回
IEnumerator<T>、IAsyncEnumerator<T>、IEnumerable<T>或IAsyncEnumerable<T>的生成类型是T。
15.15.5 枚举器对象
15.15.5.1 一般事项
当使用迭代器块实现返回枚举器接口类型或异步枚举器接口类型的函数成员或本地函数时,调用函数不会立即在迭代器块中执行代码。
而是创建并返回枚举器对象。 此对象封装迭代器块中指定的代码,并在调用枚举器对象 MoveNext 或 MoveNextAsync 方法时在迭代器块中执行代码。 枚举器对象具有以下特征:
- 它实现了
System.IDisposable、IEnumerator和IEnumerator<T>,或者System.IAsyncDisposable和IAsyncEnumerator<T>,其中T是迭代器的生成类型。 - 它使用参数值的副本(如果有)和传递给函数的实例值进行初始化。
- 它有四种可能的状态,早于、正在运行、已挂起和晚于,且最初处于早于状态。
枚举器对象通常是编译器生成的枚举器类的实例,该类封装迭代器块中的代码并实现枚举器接口,但可以实现其他实现方法。 如果枚举器类由编译器生成,该类将直接或间接嵌套在包含函数的类中,它将具有私有辅助功能,并且它将具有保留供编译器使用的名称(§6.4.3)。
枚举器对象可能实现的接口数可能多于上面指定的接口。
以下子项描述了成员推进枚举器所需的行为、从枚举器中检索当前值以及释放枚举器使用的资源。 这些分别在同步和异步枚举器的以下成员中定义:
- 要推进枚举器:
MoveNext和MoveNextAsync。 - 若要检索当前值:
Current. - 处置资源:
Dispose和DisposeAsync。
枚举器对象不支持该方法 IEnumerator.Reset 。 调用此方法会导致引发 System.NotSupportedException。
同步迭代器块和异步迭代器块的区别在于,异步迭代器成员返回任务类型,并且可以被等待。
15.15.5.2 推进枚举器
MoveNext枚举器对象的和MoveNextAsync方法封装迭代器块的代码。 调用 MoveNext 或 MoveNextAsync 方法会在迭代器块中执行代码,并根据需要设置 Current 枚举器对象的属性。
MoveNext 返回一个 bool 值,该值的含义如下所述。
MoveNextAsync 返回 a ValueTask<bool> (§15.14.3)。 从 MoveNextAsync 中返回的任务的结果值与结果 MoveNext值的含义相同。 在以下说明中,MoveNext中的操作将应用于MoveNextAsync,其差异如下:如果MoveNext返回true或false,则MoveNextAsync将任务状态设置为已完成,并将任务的结果值设置为相应的true或false值。
由 MoveNext 或 MoveNextAsync 执行的精确动作取决于调用时枚举器对象的状态:
- 如果枚举器对象的状态为开始,则调用
MoveNext:- 将状态更改为 “正在运行”。
- 将迭代器块的参数(包括
this)初始化为初始化枚举器对象时保存的参数值和实例值。 - 从头开始执行迭代器块,直到执行中断(如下所述)。
- 如果枚举器对象的状态正在运行,则调用
MoveNext的结果未指定。 - 如果枚举器对象的状态为已挂起,调用 MoveNext 则会:
- 将状态更改为 “正在运行”。
- 将所有局部变量和参数(包括
this)的值还原到上次挂起迭代器块执行时保存的值。注意:自上一次调用
MoveNext以来,这些变量引用的任何对象的内容都可能已更改。 尾注 - 在导致执行暂停的 yield return 语句之后立即恢复迭代器块的执行,并继续执行直到执行中断(如下所述)。
- 如果枚举器对象的状态在 之后,则
MoveNext调用返回 false。
当 MoveNext 执行迭代器块时,可以通过四种方式中断执行:yield return 语句、yield break 语句、遇到迭代器块的末尾以及从迭代器块中引发并传播异常。
注意:
MoveNextAsync如果await计算等待尚未完成的任务类型的表达式,则会挂起。 尾注
- 遇到
yield return语句时 (§9.4.4.20):- 语句中给出的表达式被计算,隐式转换为 yield 类型,并分配给 enumerator 对象的
Current属性。 - 迭代器主体的执行已暂停。 保存所有局部变量和参数(包括
this)的值,以及yield return语句的位置。 如果yield return语句位于一个或多个try块内,则此时不会执行关联的最终块。 - 枚举器对象的状态会变为已挂起。
- 该方法
MoveNext返回true其调用方,指示迭代成功推进到下一个值。
- 语句中给出的表达式被计算,隐式转换为 yield 类型,并分配给 enumerator 对象的
- 遇到
yield break语句时 (§9.4.4.20):-
yield break如果语句位于一个或多个try块内,则执行关联的finally块。 - 枚举器对象的状态会变为晚于。
- 该方法
MoveNext返回false给其调用方,表明迭代已完成。
-
- 遇到迭代器主体的末尾时:
- 枚举器对象的状态会变为晚于。
- 该方法
MoveNext返回false给其调用方,表明迭代已完成。
- 当异常被抛出并传播出迭代器块时:
- 迭代器主体中相应的
finally块将由异常传播执行。 - 枚举器对象的状态会变为晚于。
- 异常传播继续向方法的
MoveNext调用方传递。
- 迭代器主体中相应的
15.15.5.3 检索当前值
枚举器对象的 Current 属性受 yield return 迭代器块中的语句影响。
注意:该
Current属性是同步迭代器和异步迭代器对象的同步属性。 尾注
当枚举器对象处于 挂起 状态时,值 Current 是上一次调用 MoveNext所设置的值。 当枚举器对象处于 之前、运行中或 之后 状态时,访问 Current 的结果未定义。
对于生成类型不是object的迭代器,通过枚举器对象的Current实现访问IEnumerable的结果相当于通过枚举器对象的Current实现访问IEnumerator<T>的结果,并将其结果强制转换为object。
15.15.5.4 处置资源
方法Dispose或DisposeAsync用于通过将枚举器对象引入后状态来清理迭代。
- 如果枚举器对象的状态是之前,则调用
Dispose将状态更改为之后。 - 如果枚举器对象的状态正在运行,则调用
Dispose的结果未指定。 - 如果枚举器对象的状态为已挂起,调用
Dispose则会:- 将状态更改为 “正在运行”。
- 执行任何最终块,就像上次执行的
yield return语句是一个yield break语句一样。 如果这会导致引发异常并将其传播出迭代器正文,枚举器对象的状态将设置为 之后 ,并将异常传播到方法的Dispose调用方。 - 将状态变为晚于。
- 如果枚举器对象的状态在 之后,则
Dispose调用不会影响。
15.15.6 可枚举对象
15.15.6.1 总则
使用迭代器块实现函数成员或本地函数返回可枚举接口类型或异步可枚举接口类型时,调用函数不会立即在迭代器块中执行代码。 而是创建并返回可枚举对象。
同步可枚举对象实现 IEnumerable , IEnumerable<T>其中 T 迭代器的收益率类型。 其 GetEnumerator 方法返回枚举器对象(§15.15.5)。 异步可枚举对象实现 IAsyncEnumerable<T> 迭代器的生成类型的位置 T 。 其 GetAsyncEnumerator 方法返回异步枚举器对象(§15.15.5)。
可枚举对象使用参数值的副本(如果有)和传递给函数的实例值进行初始化。
可枚举对象通常是编译器生成的可枚举类的实例,该类封装迭代器块中的代码并实现可枚举接口,但可以实现其他实现方法。 如果编译器生成可枚举类,该类将直接或间接嵌套在包含函数的类中,它将具有私有辅助功能,并且它将具有保留供编译器使用的名称(§6.4.3)。
可枚举对象可能实现的接口可能多于上面指定的接口。
注意:例如,一个可枚举对象还可以实现
IEnumerator和IEnumerator<T>,从而使其能够同时用作可枚举和枚举器。 通常,此类实现将从第一次调用GetEnumerator返回其自己的实例(以保存分配)。 后续调用GetEnumerator(如果有)将返回一个新的类实例(通常是同一类),以便对不同枚举器实例的调用不会相互影响。 尾注
15.15.6.2 GetEnumerator 或 GetAsyncEnumerator 方法
可枚举对象提供 GetEnumerator 和 IEnumerable 接口的 IEnumerable<T> 方法的实现。 这两 GetEnumerator 种方法共享一个通用实现,该实现获取并返回一个可用的枚举器对象。 枚举器对象使用在初始化可枚举对象时保存的参数值和实例值来进行初始化,在其他方面,枚举器对象的功能如§15.15.5中所述。
异步可枚举对象提供GetAsyncEnumerator接口的IAsyncEnumerable<T>方法的实现。 此方法返回可用的异步枚举器对象。 枚举器对象使用初始化可枚举对象时保存的参数值和实例值进行初始化,包括可选的取消标记,但枚举器对象函数如 §15.15.5 中所述。 异步迭代器方法可以使用 (System.Runtime.CompilerServices.EnumeratorCancellationAttribute) 将一个参数标记为取消令牌。 实现应提供一种机制来组合取消标记,以便取消异步迭代器时,取消标记(与特性关联的自变量 GetAsyncEnumerator 或参数 System.Runtime.CompilerServices.EnumeratorCancellationAttribute)请求取消。