15 个类
15.1 常规
类是可能包含数据成员(常量和字段)、函数成员(方法、属性、事件、索引器、运算符、实例构造函数、终结器和静态构造函数)和嵌套类型的数据结构。 类类型支持继承,即派生类可以扩展和专用基类的机制。
15.2 类声明
15.2.1 常规
class_declaration是声明新类的type_declaration(§14.7)。
class_declaration
: attributes? class_modifier* 'partial'? 'class' identifier
type_parameter_list? class_base? type_parameter_constraints_clause*
class_body ';'?
;
class_declaration由一组可选的属性(§22)组成,后跟一组可选的class_modifier(§15.2.2),后跟可选partial
修饰符(§15.2.7),后跟关键字class
和命名类的标识符, 后跟可选type_parameter_list(§15.2.3),后跟可选class_base规范(§15.2.4)),后跟一组可选的type_parameter_constraints_clause(§15.2.5),后跟一个class_body(§15.2.6),(可选)后跟分号。
类声明不得提供 type_parameter_constraints_clause,除非它还提供 type_parameter_list。
提供 type_parameter_list 的类声明是泛型类声明。 此外,嵌套在泛型类声明或泛型结构声明中的任何类本身就是泛型类声明,因为应提供包含类型的类型参数来创建构造类型(§8.4)。
15.2.2 类修饰符
15.2.2.1 常规
class_declaration可以选择包括类修饰符序列:
class_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'abstract'
| 'sealed'
| 'static'
| unsafe_modifier // unsafe code support
;
unsafe_modifier (§23.2)仅在不安全的代码(§23)中可用。
同一修饰符在类声明中多次出现编译时错误。
new
允许在嵌套类上使用修饰符。 它指定类按同名隐藏继承的成员,如 §15.3.5 中所述。 修饰符在不是嵌套类声明的类声明中显示编译时错误 new
。
public
和protected
internal
private
修饰符控制类的可访问性。 根据类声明的上下文,某些修饰符可能不允许(§7.5.2)。
当分部类型声明(§15.2.7)包括辅助功能规范(通过 public
、protected
、internal
和private
修饰符)时,该规范应与包括辅助功能规范的所有其他部件一致。 如果部分类型中没有任何部分包含辅助功能规范,则会为该类型提供适当的默认辅助功能(§7.5.2)。
以下子项中讨论了 <abstract
和sealed
修饰符。
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
因此允许(但不需要)为非抽象成员。end 示例
如果类的分部类型声明(§15.2.7)的一个或多个部分包括 abstract
修饰符,则类是抽象的。 否则,该类为非抽象类。
15.2.2.3 密封类
修饰 sealed
符用于防止从类派生。 如果将密封类指定为另一类的基类,则会发生编译时错误。
密封类也不能是抽象类。
注意:
sealed
修饰符主要用于防止意外派生,但也启用某些运行时优化。 具体而言,由于密封类从不具有任何派生类,因此可以将密封类实例上的虚拟函数成员调用转换为非虚拟调用。 end note
如果类的分部类型声明(§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)。
注意:所有常量和嵌套类型都分类为静态成员。 end note
- 静态类不得具有具有
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名称是
T
窗体的typeof_expression(typeof(T)
)。
如果允许引用静态类,则允许primary_expression (§12.8)
- primary_expression形式
E
为member_access(§12.8.7)。E.I
在任何其他上下文中,引用静态类是编译时错误。
注意:例如,静态类用作基类、成员的构成类型(§15.3.7)、泛型类型参数或类型参数约束是错误的。 同样,静态类不能用于数组类型、新表达式、强制转换表达式、is 表达式、表达式、表达式、
sizeof
表达式或默认值表达式。 end note
15.2.3 类型参数
类型参数是一个简单的标识符,表示为创建构造类型而提供的类型参数的占位符。 通过 constrast,类型参数 (§8.4.2) 是创建构造类型时替换类型参数的类型。
type_parameter_list
: '<' type_parameters '>'
;
type_parameters
: attributes? type_parameter
| type_parameters ',' attributes? type_parameter
;
type_parameter在 §8.5 中定义。
类声明中的每个类型参数在该类的声明空间(§7.3)中定义一个名称。 因此,它不能与该类的另一类型参数或在该类中声明的成员同名。 类型参数不能与类型本身同名。
如果两个部分泛型类型声明(在同一程序中)具有相同的完全限定名称(包括类型参数数的generic_dimension_specifier(§12.8.18)(§7.8.3),则这两个部分泛型类型声明对同一个未绑定泛型类型的贡献。 两个此类分部类型声明应按顺序为每个类型参数指定相同的名称。
15.2.4 类基规范
15.2.4.1 常规
类声明可能包括 class_base 规范,该规范定义类的直接基类和类直接实现的接口(§18)。
class_base
: ':' class_type
| ':' interface_type_list
| ':' class_type ',' interface_type_list
;
interface_type_list
: interface_type (',' interface_type)*
;
15.2.4.2 基类
当class_type包含在class_base中时,它指定所声明的类的直接基类。 如果非分部类声明没有。 当分部类声明包含基类规范时,该基类规范应引用与包含基类规范的该分部类型所有其他部分相同的类型。 如果分部类中没有任何部分包含基类规范,则基类为 object
。 类从其直接基类继承成员,如 §15.3.4 中所述。
示例:在以下代码中
class A {} class B : A {}
类
A
据说是直接基类B
,B
据说派生自A
。 由于A
未显式指定直接基类,因此其直接基类是object
隐式的。end 示例
对于构造类类型,包括泛型类型声明(§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[]>
。end 示例
类声明中指定的基类可以是构造类类型(§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> {}
end 示例
类类型的直接基类应至少与类类型本身(§7.5.5)一样可访问。 例如,公共类派生自私有类或内部类是编译时错误。
类类型的直接基类不应为以下类型之一: System.Array
、 System.Delegate
、 System.Enum
或 System.ValueType
类型 dynamic
。 此外,泛型类声明不应 System.Attribute
用作直接或间接基类(§22.2.1)。
在确定类A
的直接基类规范B
的含义时,暂时假定B
其直接基类,这可确保基类object
规范的含义不能递归依赖自身。
示例:以下
class X<T> { public class Y{} } class Z : X<Z.Y> {}
是错误的,因为在基类规范
X<Z.Y>
中,直接基类Z
被视为object
,因此(根据 §7.8 的规则)Z
不被视为有成员Y
。end 示例
类的基类是直接基类及其基类。 换句话说,基类集是直接基类关系的可传递关闭。
示例:在以下各项中:
class A {...} class B<T> : A {...} class C<T> : B<IComparable<T>> {...} class D<T> : C<T[]> {...}
的基类
D<int>
是C<int[]>
,B<IComparable<int[]>>
和A
object
。end 示例
除类 object
外,每个类只有一个直接基类。 该 object
类没有直接基类,是所有其他类的最终基类。
这是类依赖自身的编译时错误。 出于此规则的目的,类直接依赖于其直接基类(如果有),并且直接依赖于嵌套其中最近的封闭类(如果有)。 鉴于此定义,类所依赖的完整类集是直接依赖关系的可传递关闭。
示例:示例
class A : A {}
是错误的,因为类依赖于自身。 同样,该示例
class A : B {} class B : C {} class C : A {}
是错误的,因为类循环依赖自己。 最后,示例
class A : B.C {} class B : A { public class C {} }
导致编译时错误,因为 A 依赖于
B.C
(其直接基类),这依赖于B
(它立即封闭的类),这是循环依赖A
的。end 示例
类不依赖于嵌套在其中的类。
示例:在以下代码中
class A { class B : A {} }
B
取决于A
(因为A
是它的直接基类和它的立即封闭类),但不A
依赖B
(因为B
基类和封闭类A
都不是)。 因此,该示例有效。end 示例
无法从密封类派生。
示例:在以下代码中
sealed class A {} class B : A {} // Error, cannot derive from a sealed class
类
B
出错,因为它尝试从密封类A
派生。end 示例
15.2.4.3 接口实现
class_base规范可能包括接口类型的列表,在这种情况下,类据说要实现给定的接口类型。 对于构造类类型,包括泛型类型声明(§15.3.9.7)中声明的嵌套类型,每个实现的接口类型都是通过替换给定接口中的每个type_parameter获取的,即构造类型的相应type_argument。
在多个部件(§15.2.7)中声明的类型接口集是每个部件上指定的接口的并集。 特定接口只能在每个部件上命名一次,但多个部分可以命名相同的基接口(s)。 每个给定接口的每个成员只能有一个实现。
示例:在以下各项中:
partial class C : IA, IB {...} partial class C : IC {...} partial class C : IA, IB {...}
类
C
的基接口集和IA
IB
IC
。end 示例
通常,每个部分都提供在该部件上声明的接口的实现;但是,这不是一项要求。 部件可以为在不同部件上声明的接口提供实现。
示例:
partial class X { int IComparable.CompareTo(object o) {...} } partial class X : IComparable { ... }
end 示例
类声明中指定的基接口可以构造接口类型 (§8.4, §18.2)。 基接口本身不能是类型参数,尽管它可能涉及范围中的类型参数。
示例:以下代码演示类如何实现和扩展构造类型:
class C<U, V> {} interface I1<V> {} class D : C<string, int>, I1<string> {} class E<T> : C<int, T>, I1<T> {}
end 示例
接口实现在 §18.6 中进一步讨论。
15.2.5 类型参数约束
泛型类型和方法声明可以选择通过包括 type_parameter_constraints_clauses 来指定类型参数约束。
type_parameter_constraints_clauses
: type_parameter_constraints_clause
| type_parameter_constraints_clauses type_parameter_constraints_clause
;
type_parameter_constraints_clause
: 'where' type_parameter ':' type_parameter_constraints
;
type_parameter_constraints
: primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
| secondary_constraints (',' constructor_constraint)?
| constructor_constraint
;
primary_constraint
: class_type nullable_type_annotation?
| 'class' nullable_type_annotation?
| 'struct'
| 'notnull'
| 'unmanaged'
;
secondary_constraint
: interface_type nullable_type_annotation?
| type_parameter nullable_type_annotation?
;
secondary_constraints
: secondary_constraint (',' secondary_constraint)*
;
constructor_constraint
: 'new' '(' ')'
;
每个 type_parameter_constraints_clause 都包含令牌 where
,后跟类型参数的名称,后跟冒号和该类型参数的约束列表。 每个类型参数最多可以有一个 where
子句,子 where
句可以按任意顺序列出。 get
set
与属性访问器中的标记一样,令牌where
不是关键字。
子句中 where
给定的约束列表可以包括以下任何组件,顺序如下:单个主约束、一个或多个辅助约束和构造函数约束 new()
。
主约束可以是类类型、、、class
或非托管类型约束struct
。 类类型和引用类型约束可以包括 nullable_type_annotation。
辅助约束可以是interface_type或type_parameter,可以选择后跟nullable_type_annotation。 存在 nullable_type_annotatione* 表示允许类型参数为与满足约束的非可以为 null 的引用类型相对应的可为 null 引用类型。
引用类型约束指定用于类型参数的类型参数应为引用类型。 已知为引用类型的所有类类型、接口类型、委托类型、数组类型和类型参数(如下所述)都满足此约束。
类类型、引用类型约束和辅助约束可以包含可为 null 的类型注释。 类型参数上是否存在此批注表示类型参数的可为 null 性预期:
- 如果约束不包含可为 null 的类型注释,则类型参数应为不可为 null 的引用类型。 如果类型参数为可以为 null 的引用类型,编译器可能会发出警告。
- 如果约束包含可为 null 的类型批注,则约束由不可为 null 的引用类型和可为 null 的引用类型满足。
类型参数的可为 null 性与类型参数的可为 null 性不匹配。 如果类型参数的可空性与类型实参的可空性不匹配,编译器可能会发出警告。
注意:若要指定类型参数是可为 null 的引用类型,请不要将可为 null 的类型注释添加为约束(使用
T : class
或T : BaseClass
),但在整个泛型声明中使用T?
,以指示类型参数的相应可为 null 引用类型。 end note
可为 null 的类型注释 ?
不能用于不受约束的类型参数。
对于类型参数为可为 null 的引用类型时的类型参数T
,将C?
解释为T?
,而不是C?
。C??
示例:以下示例演示类型参数的可为 null 性如何影响其类型参数声明的可为 null 性:
public class C { } public static class Extensions { public static void M<T>(this T? arg) where T : notnull { } } public class Test { public void M() { C? mightBeNull = new C(); C notNull = new C(); int number = 5; int? missing = null; mightBeNull.M(); // arg is C? notNull.M(); // arg is C? number.M(); // arg is int? missing.M(); // arg is int? } }
当类型参数为不可为 null 的类型时,
?
类型注释指示该参数是相应的可为 null 的类型。 当类型参数已是可以为 null 的引用类型时,该参数是同一可为 null 的类型。end 示例
非 null 约束指定用于类型参数的类型参数应为不可为 null 的值类型或不可为 null 的引用类型。 允许使用一个不是不可为 NULL 的值类型或不可为 NULL 的引用类型的类型参数,但编译器可能会产生诊断警告。
值类型约束指定用于类型参数的类型参数应为不可为 null 的值类型。 具有值类型约束的所有不可为 null 的结构类型、枚举类型和类型参数都满足此约束。 请注意,虽然分类为值类型,但可以为 null 的值类型(§8.3.12)不满足值类型约束。 具有值类型约束的类型参数不应也具有constructor_constraint,尽管它可用作具有constructor_constraint的另一个类型参数的类型参数。
注意:该
System.Nullable<T>
类型为 指定不可为 null 的值类型约束T
。 因此,以递归方式构造的表单T??
类型,并Nullable<Nullable<T>>
被禁止。 end note
由于unmanaged
不是关键字,因此在primary_constraint非托管约束始终与class_type在语法上不明确。 出于兼容性原因,如果名称的名称查找(unmanaged
)成功,则将其视为一种class_type
。 否则,它被视为非托管约束。
非托管类型约束指定用于类型参数的类型参数应为不可为 null 的非托管类型(§8.8)。
指针类型绝不允许为类型参数,并且不满足任何类型约束,即使是非托管类型,尽管是非托管类型。
如果约束是类类型、接口类型或类型参数,则该类型指定用于该类型参数的每个类型参数应支持的最小“基类型”。 每当使用构造的类型或泛型方法时,在编译时根据类型参数的约束检查类型参数参数。 提供的类型参数应满足 §8.4.5 中所述的条件。
class_type约束应满足以下规则:
- 该类型应为类类型。
- 类型不应为
sealed
. - 类型不应为下列类型之一:
System.Array
或System.ValueType
。 - 类型不应为
object
. - 给定类型参数的最多一个约束可能是类类型。
指定为 interface_type 约束的类型应满足以下规则:
- 该类型应为接口类型。
- 在给定
where
子句中,不应多次指定类型。
在任一情况下,约束都可能涉及关联类型的任何类型参数或方法声明作为构造类型的一部分,并且可能涉及要声明的类型。
指定为类型参数约束的任何类或接口类型应至少作为声明的泛型类型或方法可访问(§7.5.5)。
指定为 type_parameter 约束的类型应满足以下规则:
- 该类型应为类型参数。
- 在给定
where
子句中,不应多次指定类型。
此外,类型参数依赖项关系图中不应有周期,其中依赖项是以下定义的可传递关系:
- 如果类型参数用作类型参数
T
的约束,则S
S
该类型参数。T
- 如果类型参数依赖于类型参数
S
,则T
依赖于类型参数,则T
U
类型参数S
。
鉴于这种关系,类型参数依赖于自身(直接或间接)是编译时错误。
任何约束应在依赖类型参数之间保持一致。 如果类型参数 S
依赖于类型参数 T
,则:
T
不应具有值类型约束。 否则,将有效地密封,T
因此S
将被强制为同一类型T
,因此不需要两个类型参数。- 如果
S
具有值类型约束,则T
不应具有 class_type 约束。 - 如果具有class_type约束,并且
S
具有class_type约束A
,则应当有标识转换或隐式引用转换T
,或者从B
中隐式引用转换。A
B
B
A
- 如果
S
还依赖于类型参数U
并具有U
约束,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或同时具有constructor_constraint,这是一个编译时错误。struct
unmanaged
示例:下面是约束示例:
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 { ... }
end 示例
类型的动态擦除C
是按如下所示构造的类型Cₓ
:
- 如果是
C
嵌套类型Outer.Inner
,则Cₓ
为嵌套类型Outerₓ.Innerₓ
。 - 如果是
C
Cₓ
具有类型参数G<A¹, ..., Aⁿ>
A¹, ..., Aⁿ
的构造类型Cₓ
,则为构造类型G<A¹ₓ, ..., Aⁿₓ>
。 - 如果是
C
数组类型E[]
Cₓ
,则为数组类型Eₓ[]
。 - 如果是
C
动态的,Cₓ
则为object
. - 否则
Cₓ
为C
。
类型定义如下:
让我们 R
成为一组类型,以便:
- 对于每个约束
T
都是一个类型参数,R
包含其有效的基类。 - 对于每个约束
T
都是一个结构类型,R
包含System.ValueType
。 - 对于每个约束
T
都是一个枚举类型,R
包含System.Enum
。 - 对于每个约束
T
都是委托类型,R
包含其动态擦除。 - 对于每个约束
T
都是数组类型,R
包含System.Array
。 - 对于该类
T
类型的每个约束,R
包含其动态擦除。
Then
- 如果
T
具有值类型约束,则其有效基类为System.ValueType
。 - 否则,如果
R
为空,则有效基类为object
。 - 否则,有效基类
T
是集内最包含的类型(R
)。 如果集没有包含类型,则有效基类为T
object
. 一致性规则确保存在最包含的类型。
如果类型参数是一个方法类型参数,其约束是从基方法继承的,则有效基类在类型替换后计算。
这些规则可确保有效的基类始终是 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约束的有效接口集的联合。
如果类型参数。 如果已知类型为引用类型且具有不可为 null 的引用类型约束,则类型参数已知为不可为 null 的引用类型。
约束类型参数类型的值可用于访问约束隐含的实例成员。
示例:在以下各项中:
interface IPrintable { void Print(); } class Printer<T> where T : IPrintable { void PrintOne(T x) => x.Print(); }
可以直接调用方法
IPrintable
,因为x
限制为始终实现T
。IPrintable
end 示例
当部分泛型类型声明包含约束时,约束应与包含约束的所有其他部分一致。 具体而言,包含约束的每个部分应具有相同的类型参数集的约束,对于每种类型参数,主要、辅助和构造函数约束集应等效。 如果两组约束包含相同的成员,则两组约束是等效的。 如果部分泛型类型没有指定类型参数约束,则类型参数被视为不受约束。
示例:
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> { ... }
正确,因为这些部分包含约束(前两个)有效地为同一组类型参数指定相同的主、辅助和构造函数约束集。
end 示例
15.2.6 类正文
类的class_body定义该类的成员。
class_body
: '{' class_member_declaration* '}'
;
15.2.7 分部声明
在多个部件中定义类、结构或接口类型时,将使用修饰符 partial
。 partial
修饰符是上下文关键字(§6.4.4),仅在其中一个关键字或关键字class
struct
interface
之前具有特殊含义。
分部类型声明的每个部分应包含修饰符,并且应在同一partial
命名空间中声明或包含与其他部分相同的类型。 partial
修饰符指示类型声明的其他部分可能存在于其他位置,但存在此类附加部件并不是必需的;它对于包含修饰符的类型的唯一partial
声明有效。 它只对部分类型的一个声明有效,以包含基类或实现的接口。 但是,基类或实现接口的所有声明都必须匹配,包括任何指定类型参数的可为 null 性。
部分类型的所有部分应一起编译,以便可以在编译时合并这些部件。 具体来说,部分类型不允许扩展已编译的类型。
可以使用修饰符在多个部件 partial
中声明嵌套类型。 通常,也使用 partial
声明包含类型,并且嵌套类型的每个部分都声明在包含类型的不同部分。
示例:以下分部类在两个部分实现,它们驻留在不同的编译单元中。 第一部分是由数据库映射工具生成的计算机,而第二部分是手动创作的第二部分:
public partial class Customer { private int id; private string name; private string address; private List<Order> orders; public Customer() { ... } } // File: Customer2.cs public partial class Customer { public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted); public bool HasOutstandingOrders() => orders.Count > 0; }
当上述两个部分一起编译时,生成的代码的行为就像类已编写为单个单元一样,如下所示:
public class Customer { private int id; private string name; private string address; private List<Order> orders; public Customer() { ... } public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted); public bool HasOutstandingOrders() => orders.Count > 0; }
end 示例
§22.3 中讨论了对分部声明的不同部分的类型或类型参数指定的属性的处理。
15.3 类成员
15.3.1 常规
类的成员由其 class_member_declaration和从直接基类继承的成员组成。
class_member_declaration
: constant_declaration
| field_declaration
| method_declaration
| property_declaration
| event_declaration
| indexer_declaration
| operator_declaration
| constructor_declaration
| finalizer_declaration
| static_constructor_declaration
| type_declaration
;
类的成员分为以下类别:
- 常量,表示与类关联的常量值(§15.4)。
- 字段是类的变量(§15.5)。
- 实现类(§15.6)可执行的计算和操作的方法。
- 属性,用于定义命名特征以及与读取和写入这些特征相关的操作(§15.7)。
- 事件,用于定义可由类 (§15.8) 生成的通知。
- 索引器,它允许以与数组(§15.9)相同的方式(语法上)对类的实例进行索引。
- 运算符,用于定义可应用于类实例(§15.10)的表达式运算符。
- 实例构造函数,它实现初始化类实例所需的操作(§15.11)
- 终结器,用于实现在永久丢弃类实例之前要执行的操作(§15.13)。
- 静态构造函数,它实现初始化类本身所需的操作(§15.12)。
- 类型,表示类(§14.7)的本地类型。
class_declaration创建新的声明空间(§7.3),type_parameter和class_member_declaration立即由class_declaration将新成员引入此声明空间。 以下规则适用于 class_member_declarations:
实例构造函数、终结器和静态构造函数的名称应与立即封闭的类相同。 所有其他成员的名称应不同于立即封闭类的名称。
类声明type_parameter_list中类型参数的名称应与同一type_parameter_list中所有其他类型参数的名称不同,并且与类的名称和类的所有成员的名称不同。
类型的名称应不同于在同一类中声明的所有非类型成员的名称。 如果两个或更多类型声明共享相同的完全限定名称,则声明应具有
partial
修饰符(§15.2.7),这些声明组合在一起以定义单个类型。
注意:由于类型声明的完全限定名称对类型参数的数目进行编码,因此,只要类型参数数量不同,两个不同的类型可以共享相同的名称。 end note
常量、字段、属性或事件的名称应不同于同一类中声明的所有其他成员的名称。
方法的名称应不同于同一类中声明的所有其他非方法的名称。 此外,方法的签名(§7.6)应不同于同一类中声明的所有其他方法的签名,同一类中声明的两个方法不得具有唯
in
一不同的签名,out
并且ref
。实例构造函数的签名应不同于在同一类中声明的所有其他实例构造函数的签名,在同一类中声明的两个构造函数不应具有唯
ref
out
一不同的签名。索引器的签名应不同于在同一类中声明的所有其他索引器的签名。
运算符的签名应不同于在同一类中声明的所有其他运算符的签名。
类(§15.3.4)的继承成员不是类的声明空间的一部分。
注意:因此,允许派生类声明与继承成员同名或签名的成员(实际上隐藏继承的成员)。 end note
在多个部分(§15.2.7)中声明的类型的成员集是每个部件中声明的成员的并集。 类型声明的所有部分的主体共享相同的声明空间(§7.3),每个成员(§7.7)的范围扩展到所有部件的主体。 任何成员的辅助功能域始终包括封闭类型的所有部分;一个部件中声明的私有成员可从另一个部件自由访问。 除非该成员是具有 partial
修饰符的类型,否则在类型的多个部分中声明同一成员是编译时错误。
示例:
partial class A { int x; // Error, cannot declare x more than once partial class Inner // Ok, Inner is a partial type { int y; } } partial class A { int x; // Error, cannot declare x more than once partial class Inner // Ok, Inner is a partial type { int z; } }
end 示例
字段初始化顺序在 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
end 示例
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[,][]
end 示例
在实例函数成员中,其类型 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 } }
end 示例
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>
具有通过替换类型参数int
的类型参数G(string s)
获取的非继承成员int
T
。D<int>
还有一个从类声明B
继承的成员。 此继承成员首先通过替换B<int[]>
D<int>
基类规范int
中的基类类型T
B<T[]>
来确定。 然后,作为类型参数B
int[]
替换为U
inpublic U F(long index)
,从而生成继承的成员public int[] F(long index)
。end 示例
15.3.5 新修饰符
允许class_member_declaration声明与继承成员具有相同名称或签名的成员。 发生这种情况时,将据说 派生类成员隐藏 基类成员。 有关成员隐藏继承成员时的精确规范,请参阅 §7.7.2.3 。
如果可访问,则继承的成员M
被视为可用,并且没有其他继承的可访问成员 N 已隐藏M
。M
隐式隐藏继承的成员不被视为错误,但编译器发出警告,除非派生类成员的声明包括一个 new
修饰符,以明确指示派生成员旨在隐藏基成员。 如果嵌套类型的一个或多个部分声明(§15.2.7)包含 new
修饰符,则嵌套类型隐藏可用继承成员时不会发出警告。
new
如果修饰符包含在不隐藏可用继承成员的声明中,则会发出该效果的警告。
15.3.6 访问修饰符
class_member_declaration可以具有任何一种允许的声明可访问性(§7.5.2):、、、public
、protected internal
protected
、或private protected
internal
。private
除了 protected internal
和 private protected
组合之外,指定多个访问修饰符是编译时错误。 假设class_member_declaration不包含任何访问修饰符private
。
15.3.7 构成类型
在成员声明中使用的类型称为 该成员的构成类型 。 可能的构成类型是常量、字段、属性、事件或索引器的类型、方法或运算符的返回类型,以及方法、索引器、运算符或实例构造函数的参数类型。 成员的构成类型应至少与该成员本身(§7.5.5)一样可访问。
15.3.8 静态成员和实例成员
类的成员是静态成员或实例成员。
注意:一般来说,将静态成员视为属于类和实例成员属于对象(类实例)很有用。 end note
当字段、方法、属性、事件、运算符或构造函数声明包含 static
修饰符时,它将声明静态成员。 此外,常量或类型声明隐式声明静态成员。 静态成员具有以下特征:
- 在表单
M
的member_access(§12.8.7)中引用静态成员E.M
时,E
应表示具有成员M
的类型。 这是表示实例的编译时错误E
。 - 非泛型类中的静态字段确切标识一个存储位置。 无论创建了多少个非泛型类的实例,静态字段只有一个副本。 每个不同的封闭构造类型(§8.4.3)都有自己的静态字段集,而不考虑封闭构造类型的实例数。
- 静态函数成员(方法、属性、事件、运算符或构造函数)不在特定实例上运行,在此类函数成员中引用此方法是编译时错误。
当字段、方法、属性、事件、索引器、构造函数或终结器声明不包含静态修饰符时,它将声明实例成员。 (实例成员有时称为非静态成员。实例成员具有以下特征:
- 在表单
M
的member_access(§12.8.7)中引用实例成员E.M
时,E
应表示具有成员M
的类型实例。 E 表示类型是绑定时错误。 - 类的每个实例都包含一组单独的类的所有实例字段。
- 实例函数成员(方法、属性、索引器、实例构造函数或终结器)对类的给定实例进行操作,并且此实例可以作为 (
this
) 访问。
示例:以下示例演示了访问静态成员和实例成员的规则:
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)中,实例成员应通过实例访问,静态成员应通过类型进行访问。end 示例
15.3.9 嵌套类型
15.3.9.1 常规
在类或结构中声明的类型称为嵌套类型。 编译单元或命名空间中声明的类型称为 非嵌套类型。
示例:在以下示例中:
class A { class B { static void F() { Console.WriteLine("A.B.F"); } } }
类
B
是嵌套类型,因为它在类A
中声明,类A
是非嵌套类型,因为它在编译单元中声明。end 示例
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
。end 示例
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
。end 示例
15.3.9.5 此访问权限
嵌套类型及其包含类型与this_access(§12.8.14)没有特殊关系。 具体而言, this
嵌套类型内不能用于引用包含类型的实例成员。 如果嵌套类型需要访问其包含类型的实例成员,可以通过提供包含类型的实例作为嵌套类型的构造函数参数来提供 this
访问权限。
示例:以下示例
class C { int i = 123; public void F() { Nested n = new Nested(this); n.G(); } public class Nested { C this_c; public Nested(C c) { this_c = c; } public void G() { Console.WriteLine(this_c.i); } } } class Test { static void Main() { C c = new C(); c.F(); } }
显示此技术。 创建实例的
C
一个实例,并将自己的此Nested
实例Nested
传递给其构造函数,以便提供对C
实例成员的后续访问。end 示例
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
具有私有声明的辅助功能。end 示例
嵌套类型还可以访问在其包含类型的基类型中定义的受保护成员。
示例:在以下代码中
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
。end 示例
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 } }
end 示例
尽管编程样式不正确,但嵌套类型中的类型参数可以隐藏在外部类型中声明的成员或类型参数。
示例:
class Outer<T> { class Inner<T> // Valid, hides Outer's T { public T t; // Refers to Inner's T } }
end 示例
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# 实现中保持一致。
end note
终结器(§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
end 示例
15.3.10.3 为事件保留的成员名称
void add_E(T handler);
void remove_E(T handler);
15.3.10.4 为索引器保留的成员名称
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可能包括一组属性(§22)、修饰符(new
),以及任何一种允许的声明辅助功能(§15.3.6)。 属性和修饰符适用于constant_declaration声明的所有成员。 即使常量被视为静态成员, constant_declaration 既不需要也不允许 static
修饰符。 同一修饰符在常量声明中出现多次是错误的。
constant_declaration的类型指定声明引入的成员的类型。 该类型后跟constant_declarators (§13.6.3),每个列表都会引入一个新成员。 constant_declarator包含一个标识符,该标识符命名成员,后跟一个“”令牌,后跟一个=
(§12.23)。
常量声明中指定的类型应为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
成员声明、属性和其他常量声明。 end note
注意:如 §12.23 中所述,constant_expression是可在编译时完全计算的表达式。 由于创建非 null 值的唯一方法是应用运算符以外的
string
reference_typenew
end note
如果需要常量值的符号名称,但在常量声明中不允许该值的类型,或者当constant_expression在编译时无法计算该值时,可以改用只读字段(§15.5.3)。
注意:版本控制语义
const
和readonly
不同 (§15.5.3.3.3)。 end note
声明多个常量的常量声明等效于具有相同属性、修饰符和类型的单个常量的多个声明。
示例:
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; }
end 示例
只要依赖项不是循环性质,常量就允许依赖于同一程序中的其他常量。
示例:在以下代码中
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
。end 示例
常量声明可能依赖于来自其他程序的常量,但此类依赖项只能在一个方向上实现。
示例:引用上面的示例,如果在
A
单独的程序中声明并B
声明,则可能需要A.X
依赖B.Z
,但B.Z
不能同时依赖A.Y
。 end 示例
15.5 字段
15.5.1 常规
字段是表示与对象或类关联的变量的成员。 field_declaration引入了给定类型的一个或多个字段。
field_declaration
: attributes? field_modifier* type variable_declarators ';'
;
field_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'readonly'
| 'volatile'
| unsafe_modifier // unsafe code support
;
variable_declarators
: variable_declarator (',' variable_declarator)*
;
variable_declarator
: identifier ('=' variable_initializer)?
;
unsafe_modifier (§23.2)仅在不安全的代码(§23)中可用。
field_declaration可能包括一组属性(§22)、修饰符(new
)、四个访问修饰符(§15.3.6)的有效组合和修饰符(static
)。 此外,field_declaration可能包括修饰符(§15.5.3)或readonly
修饰符(§15.5.4),但不包括两者。volatile
属性和修饰符适用于field_declaration声明的所有成员。 同一修饰符在field_declaration中出现多次是错误的。
field_declaration的类型指定声明引入的成员的类型。 该类型后跟variable_declarator列表,每个列表都会引入一个新成员。 variable_declarator包含一个标识符,该标识符命名该成员,可以选择后跟一个“”令牌和一个=
(§15.5.6)。
字段的类型应至少与字段本身(§7.5.5.5)一样可访问。
字段的值是在表达式中使用simple_name(§12.8.4)、member_access(§12.8.7)或base_access(§12.8.15)获取的。 使用赋值(§12.21)修改非只读字段的值。 可以使用后缀递增和递减运算符(§12.8.16)和前缀增量和递减运算符(§12.9.6)获取和修改非只读字段的值。
声明多个字段的字段声明等效于具有相同属性、修饰符和类型的单个字段的多个声明。
示例:
class A { public static int X = 1, Y, Z = 100; }
等效于
class A { public static int X = 1; public static int Y; public static int Z = 100; }
end 示例
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
会具有完全相同的效果。end 示例
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
将不受影响。end 示例
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
示例:示例
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
字段可防止出现任何此类不一致。end 示例
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
者都自动初始化为默认值。end 示例
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
。end 示例
§15.5.5 中所述的默认值初始化针对所有字段(包括具有变量初始值设定项的字段)进行。 因此,初始化类时,该类中的所有静态字段首先初始化为默认值,然后以文本顺序执行静态字段初始值设定项。 同样,创建类的实例时,该实例中的所有实例字段首先初始化为其默认值,然后实例字段初始值设定项按文本顺序执行。 如果同一类型的多个分部类型声明中存在字段声明,则未指定各部分的顺序。 但是,在每个部分内,字段初始值设定项按顺序执行。
可以观察具有变量初始值设定项的静态字段的默认值状态。
示例:但是,强烈建议不要将其作为样式问题。 示例
class Test { static int a = b + 1; static int b = a + 1; static void Main() { Console.WriteLine($"a = {a}, b = {b}"); } }
展示此行为。 尽管有循环定义
a
,但b
程序仍然有效。 它会导致输出a = 1, b = 2
因为静态字段
a
并在b
执行初始值设定项之前初始化为0
(默认值int
)。 运行初始值a
设定项时,值为b
零,因此a
初始化为1
。 当运行初始值设定项b
时,值已经1
,因此b
初始化为2
。end 示例
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
运行。end 示例
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
,因为它引用正在创建的实例的成员。end 示例
15.6 方法
15.6.1 常规
方法是实现对象或类可执行的计算或操作的成员。 使用 method_declarations 声明方法:
method_declaration
: attributes? method_modifiers return_type method_header method_body
| attributes? ref_method_modifiers ref_kind ref_return_type method_header
ref_method_body
;
method_modifiers
: method_modifier* 'partial'?
;
ref_kind
: 'ref'
| 'ref' 'readonly'
;
ref_method_modifiers
: ref_method_modifier*
;
method_header
: member_name '(' parameter_list? ')'
| member_name type_parameter_list '(' parameter_list? ')'
type_parameter_constraints_clause*
;
method_modifier
: ref_method_modifier
| 'async'
;
ref_method_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
return_type
: ref_return_type
| 'void'
;
ref_return_type
: type
;
member_name
: identifier
| interface_type '.' identifier
;
method_body
: block
| '=>' null_conditional_invocation_expression ';'
| '=>' expression ';'
| ';'
;
ref_method_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
语法说明:
- unsafe_modifier (§23.2)仅在不安全的代码(§23)中可用。
- 如果null_conditional_invocation_expression和表达式替代项均适用,则选择前者时识别method_body。
注意:此处的替代项的重叠和优先级只是为了描述性便利;可以详细说明语法规则以删除重叠。 ANTLR 和其他语法系统采用相同的便利性,因此 method_body 自动具有指定的语义。 end note
method_declaration可能包括一组属性(§22)和一种允许的声明辅助功能(§15.3.6)、(§15.3.5)、new
(§15.6.3)、 static
(§15.6.3virtual
、 (override
)、 (sealed
、(§15.6.7)、 abstract
(§15.6.8)和extern
(§15.15)修饰符。
如果以下所有内容都为 true,则声明具有修饰符的有效组合:
- 声明包括访问修饰符(§15.3.6)的有效组合。
- 声明不包含同一修饰符多次。
- 声明最多包含以下修饰符之一:
static
、virtual
和override
。 - 声明最多包含以下修饰符之一:
new
和override
。 - 如果声明包含
abstract
修饰符,则声明不包含以下任何修饰符:static
、virtual
、sealed
或extern
。 - 如果声明包含
private
修饰符,则声明不包含以下任何修饰符:virtual
、override
或abstract
。 - 如果声明包含
sealed
修饰符,则声明还包括override
修饰符。 - 如果声明包含
partial
修饰符,则它不包含以下任何修饰符:new
、、public
、、protected
、internal
、private
virtual
sealed
override
或。abstract
extern
方法根据返回的内容(如果有)进行分类:
- 如果
ref
存在,则该方法按 ref 返回并返回变量引用,该引用是可选的只读; - 否则,如果return_type
void
为,则该方法返回为 no-value 且不返回值; - 否则,该方法按 值 返回并返回一个值。
returns-by-value 或 returns-no-value 方法声明的return_type指定方法返回的结果类型(如果有)。 只有 returns-no-value 方法可以包含 partial
修饰符(§15.6.9)。 如果声明包含 async
修饰符 ,则return_type 应 void
或方法按值返回,并且返回类型为 任务类型 (§15.15.1)。
returns-by-ref 方法声明的ref_return_type指定由该方法返回的variable_reference引用的变量的类型。
泛型方法是其声明包括 type_parameter_list的方法。 这指定方法的类型参数。 可选 type_parameter_constraints_clause指定类型参数的约束。
显式接口成员实现的泛型 method_declaration 不应有任何 type_parameter_constraints_clause;声明从接口方法的约束继承任何约束。
同样,具有 override
修饰符的方法声明不应有任何 type_parameter_constraints_clause,并且方法的类型参数的约束继承自被重写的虚拟方法。
member_name指定方法的名称。 除非该方法是显式接口成员实现(§18.6.2),否则member_name只是标识符。
对于显式接口成员实现,member_name由后跟“”和.
的interface_type组成。 在这种情况下,声明不得包含除(可能) extern
或 async
以外的任何修饰符。
可选 parameter_list 指定方法的参数(§15.6.2)。
return_type或ref_return_type,以及方法parameter_list中引用的每个类型,应至少与方法本身(§7.5.5)一样可访问。
returns-by-value 或 returns-no-value 方法的method_body是分号、块体或表达式正文。 块正文由一个 块组成,该块指定要在调用该方法时执行的语句。 表达式正文由 =>
后跟 null_conditional_invocation_expression 或 表达式以及分号组成,并表示调用方法时要执行的单个表达式。
对于抽象和外部方法, method_body 只包含分号。 对于分部方法, method_body 可能包含分号、块体或表达式正文。 对于所有其他方法, method_body 是块体或表达式正文。
如果method_body由分号组成,则声明不应包含async
修饰符。
returns-by-ref 方法的ref_method_body是分号、块体或表达式正文。 块正文由一个 块组成,该块指定要在调用该方法时执行的语句。 表达式正文由后跟=>
和分号组成ref
,并表示调用方法时要计算的单个variable_reference。
对于抽象和外部方法, ref_method_body 只包含分号;对于所有其他方法, ref_method_body 是块体或表达式体。
名称、类型参数数和方法的参数列表定义方法的签名(§7.6)。 具体而言,方法的签名包括其名称、其类型参数的数目以及数字、parameter_mode_modifier s(§15.6.2.1)及其参数的类型。 返回类型不是方法签名的一部分,也不是参数的名称、类型参数的名称或约束。 当参数类型引用方法的类型参数时,类型参数(而不是类型参数的名称)的序号位置用于类型等效。
方法的名称应不同于同一类中声明的所有其他非方法的名称。 此外,方法的签名应不同于同一类中声明的所有其他方法的签名,在同一类中声明的两个方法不得具有唯in
out
一不同的签名,并且ref
。
方法的type_parameter范围在整个method_declaration范围内,可用于在return_type或ref_return_type、method_body或ref_method_body范围内形成类型,以及type_parameter_constraints_clause,但不能在属性中形成类型。
所有参数和类型参数应具有不同的名称。
15.6.2 方法参数
15.6.2.1 常规
方法parameter_list声明方法的参数(如果有)。
parameter_list
: fixed_parameters
| fixed_parameters ',' parameter_array
| parameter_array
;
fixed_parameters
: fixed_parameter (',' fixed_parameter)*
;
fixed_parameter
: attributes? parameter_modifier? type identifier default_argument?
;
default_argument
: '=' expression
;
parameter_modifier
: parameter_mode_modifier
| 'this'
;
parameter_mode_modifier
: 'ref'
| 'out'
| 'in'
;
parameter_array
: attributes? 'params' array_type identifier
;
参数列表由一个或多个逗号分隔的参数组成,其中只有最后一个 参数可能是parameter_array。
fixed_parameter由一组可选属性(§22)、可选in
、out
ref
或this
修饰符组成;类型;标识符;可选default_argument。 每个 fixed_parameter 都声明具有给定名称的给定类型的参数。 this
修饰符将该方法指定为扩展方法,并且仅在非泛型非嵌套静态类中静态方法的第一个参数上允许。 如果参数是受约束为 a struct
的类型或类型参数,则struct
修饰符可以与修饰this
符或修饰符组合,但不能ref
与in
修饰符out
组合。 扩展方法在 §15.6.10 中进一步介绍。 具有default_argument的fixed_parameter称为可选参数,而没有default_argument的fixed_parameter是必需参数。 必需的参数不应出现在parameter_list中的可选参数之后。
具有ref
或out
this
修饰符的参数不能具有default_argument。 输入参数可能具有 default_argument。 default_argument中的表达式应为下列表达式之一:
- constant_expression
- 窗体的
new S()
表达式,其中S
是值类型 - 窗体的
default(S)
表达式,其中S
是值类型
表达式应由标识或可为 null 的转换隐式转换为参数的类型进行隐式转换。
如果可选参数出现在实现部分方法声明 (§15.6.9)、显式接口成员实现 (§18.6.2)、单参数索引器声明 (§15.9) 或运算符声明 (§15.10.1) 中,编译器会发出警告,因为这些成员永远不能以允许省略参数的方式调用。
parameter_array由一组可选的属性(§22)、params
修饰符、array_type和标识符组成。 参数数组使用给定名称声明给定数组类型的单个参数。 参数数组的array_type应为单维数组类型(§17.2)。 在方法调用中,参数数组允许指定给定数组类型的单个参数,或者允许指定数组元素类型的零个或多个参数。 参数数组在 §15.6.2.4 中进一步介绍。
parameter_array可能发生在可选参数之后,但不能有默认值 - parameter_array的参数遗漏会导致创建空数组。
示例:下面说明了不同类型的参数:
void M<T>( ref int i, decimal d, bool b = false, bool? n = false, string s = "Hello", object o = null, T t = default(T), params int[] a ) { }
在parameter_list
M
,i
是必需参数,ref
是必需d
值参数,b
s
o
并且t
是可选值参数,并且a
是参数数组。end 示例
方法声明为参数和类型参数创建单独的声明空间(§7.3)。 名称由类型参数列表和方法的参数列表引入此声明空间。 方法的正文(如果有)被视为嵌套在此声明空间中。 方法声明空间的两个成员具有相同名称是错误的。
方法调用(§12.8.10.2)创建一个特定于该方法的参数和局部变量的副本,调用的参数列表为新创建的参数或变量引用赋值或变量引用。 在方法块中,参数可以通过simple_name表达式中的标识符(§12.8.4)引用。
存在以下类型的参数:
- 值参数(§15.6.2.2)。
- 输入参数(§15.6.2.3.2)。
- 输出参数 (§15.6.2.3.4)。
- 引用参数 (§15.6.2.3.3)。
- 参数数组(§15.6.2.4)。
注意:如 §7.6 中所述,
in
out
和ref
修饰符是方法签名的一部分,但params
修饰符不是。 end note
15.6.2.2 值参数
不带修饰符声明的参数是值 参数。 值参数是一个局部变量,它从方法调用中提供的相应参数中获取其初始值。
有关明确赋值规则,请参阅 §9.2.5。
方法调用中的相应参数应是隐式转换为参数类型的表达式(§10.2)。
允许将新值分配给值参数的方法。 此类分配仅影响值参数表示的本地存储位置,它们对方法调用中给定的实际参数没有影响。
15.6.2.3 按引用参数
15.6.2.3.1 常规
输入、输出和引用参数是 按引用参数。 按引用参数是本地引用变量(§9.7);初始引用是从方法调用中提供的相应参数获取的。
注意:可以使用 ref assignment (
= ref
) 运算符更改按引用参数的引用。
当参数是按引用参数时,方法调用中的相应参数应由与参数类型相同的variable_reference(§9.5)包含相应的关键字、in
或后跟ref
(out
)。 但是,当参数是in
参数时,该参数可能是从该参数表达式到相应参数的类型存在隐式转换(§10.2)的表达式。
对于声明为迭代器(§15.14)或异步函数(§15.15)的函数,不允许按引用参数。
在采用多个按引用参数的方法中,多个名称可以表示相同的存储位置。
15.6.2.3.2 输入参数
使用 in
修饰符声明的参数是输入 参数。 与输入参数对应的参数是方法调用点存在的变量,或者是方法调用中由实现(§12.6.2.2.3)创建的变量。 有关明确赋值规则,请参阅 §9.2.8。
修改输入参数的值是编译时错误。
注意:输入参数的主要用途是提高效率。 当方法参数的类型是大型结构(在内存要求方面),在调用该方法时,避免复制参数的整个值非常有用。 输入参数允许方法引用内存中的现有值,同时提供对这些值的不需要更改的保护。 end note
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
对于 in 的调用
Swap
,Main
表示x
和i
表示y
。j
因此,调用具有交换值i
和j
。end 示例
示例:在以下代码中
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
。end 示例
struct
对于类型,在实例方法、实例访问器(§12.2.1)或具有构造函数初始值设定项的实例构造函数中,this
关键字的行为与结构类型的引用参数(§12.8.14)完全相同。
15.6.2.3.4 输出参数
使用 out
修饰符声明的参数是 输出参数。 有关明确赋值规则,请参阅 §9.2.7。
声明为分部方法(§15.6.9)的方法不应具有输出参数。
注意:输出参数通常用于生成多个返回值的方法。 end note
示例:
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
,并在调用后将其视为明确分配。end 示例
15.6.2.4 参数数组
使用 params
修饰符声明的参数是参数数组。 如果参数列表包含参数数组,则它应为列表中的最后一个参数,并且它应为单维数组类型。
示例:类型和
string[]
string[][]
可用作参数数组的类型,但类型string[,]
不能。 end 示例
注意:无法将
params
修饰符与修饰符in
合并,out
或者ref
。 end note
参数数组允许在方法调用中通过以下两种方式之一指定参数:
- 为参数数组提供的参数可以是隐式转换为参数数组类型的单个表达式(§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[] {});
end 示例
执行重载解析时,具有参数数组的方法可能适用,可以是正常形式,也可以采用扩展形式(§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[])
在此示例中,具有参数数组的方法的两种可能的扩展形式已作为常规方法包含在类中。 因此,在执行重载解析时,不会考虑这些扩展形式,因此第一个和第三个方法调用会选择常规方法。 当类使用参数数组声明方法时,还不常见地包括一些扩展形式作为常规方法。 这样做可以避免在调用具有参数数组的方法的扩展形式时发生的数组实例的分配。
end 示例
数组是引用类型,因此为参数数组传递的值可以是
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 引用的数组。end 示例
当参数数组的类型为 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[]
的引用)进行初始化。end 示例
15.6.3 静态和实例方法
当方法声明包含 static
修饰符时,该方法据说是静态方法。 当不存在 static
修饰符时,该方法称为实例方法。
静态方法不对特定实例进行操作,它是在静态方法中引用 this
的编译时错误。
实例方法对类的给定实例进行操作,并且该实例可以作为 (this
) 访问。
静态成员和实例成员之间的差异在 §15.3.8 中进一步讨论。
15.6.4 虚拟方法
当实例方法声明包含虚拟修饰符时,该方法称为虚拟方法。 当不存在虚拟修饰符时,该方法称为 非虚拟方法。
非虚拟方法的实现是固定的:无论该方法是在声明它的类的实例上调用还是派生类的实例上调用,实现都是相同的。 相比之下,虚拟方法的实现可由派生类取代。 取代继承的虚拟方法实现的过程称为 重写 该方法(§15.6.5)。
在虚拟方法调用中, 进行调用的实例的运行时类型 决定了要调用的实际方法实现。 在非虚拟方法调用中, 实例的编译时类型 是确定因素。 确切地说,当在具有编译时类型和运行时类型的N
A
实例上使用参数列表C
调用命名R
的方法(其中或R
C
派生自C
的类)时,调用将按如下方式进行处理:
- 在绑定时,重载解析应用于 ,并从中声明和继承的方法
C
集中选择特定方法N
。A
M
C
这在 §12.8.10.2 中介绍。 - 然后在运行时:
- 如果
M
为非虚拟方法,M
则调用。 - 否则,
M
是一种虚拟方法,调用其M
最派生的R
实现。
- 如果
对于类声明或继承的每个虚拟方法,该类存在 该方法的大多数派生实现 。 与类M
相关的虚拟方法R
的最派生实现按如下方式确定:
- 如果
R
包含引入的虚拟声明M
,则这是与它相关的M
最派生的实现R
。 - 否则,如果
R
包含重写M
,则这是相对于M
的派生实现R
。 - 否则,相对于直接基类
M
而言,最派生的R
M
实现与最派生的R
实现相同。
示例:以下示例说明了虚拟和非虚拟方法之间的差异:
class A { public void F() => Console.WriteLine("A.F"); public virtual void G() => Console.WriteLine("A.G"); } class B : A { public new void F() => Console.WriteLine("B.F"); public override void G() => Console.WriteLine("B.G"); } class Test { static void Main() { B b = new B(); A a = b; a.F(); b.F(); a.G(); b.G(); } }
在此示例中,
A
引入了非虚拟方法和F
虚拟方法G
。 该类B
引入了新的非虚拟方法,从而F
继承的方法F
,并重写继承的方法G
。 该示例生成输出:A.F B.F B.G B.G
请注意,该语句
a.G()
调用,B.G
而不是A.G
。 这是因为实例(即B
),而不是实例(即)的编译时类型(即A
)确定要调用的实际方法实现。end 示例
由于允许方法隐藏继承的方法,因此类可以包含多个具有相同签名的虚拟方法。 这并不存在歧义问题,因为除了最派生的方法,所有方法都隐藏。
示例:在以下代码中
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
来调用隐藏的虚拟方法。end 示例
15.6.5 重写方法
当实例方法声明包含 override
修饰符时,该方法称为 替代方法。 重写方法重写具有相同签名的继承的虚拟方法。 虽然虚拟方法声明 引入了 新方法,但重写方法声明 通过提供该方法的新实现来专用 于现有的继承的虚拟方法。
重写声明重写的方法称为,重写基方法通过检查每个基类(从直接基类M
开始并继续每个连续直接基类)来确定,直到给定基类类型中至少有一个可访问的方法C
位于该基类类型中,其签名C
与替换类型参数后相同。 为了查找重写的基方法,如果方法是public
、如果是protected
、protected internal
如果是,或者internal
private protected
它是或在同一程序中C
声明的方法,则被视为可访问该方法。
除非重写声明存在以下所有情况,否则会发生编译时错误:
- 可按上文所述找到重写的基方法。
- 正是有一种这种重写的基方法。 仅当基类类型是构造类型时,此限制才有效,其中类型参数的替换使两个方法的签名相同。
- 重写的基方法是虚拟、抽象或重写方法。 换句话说,重写的基方法不能是静态方法或非虚拟方法。
- 重写的基方法不是密封方法。
- 重写基方法和重写方法的返回类型之间存在标识转换。
- 重写声明和重写的基方法具有相同声明的可访问性。 换句话说,重写声明无法更改虚拟方法的可访问性。 但是,如果重写的基方法在内部受到保护,并且它在与包含替代声明的程序集不同的程序集中声明,则重写声明的声明可访问性应受到保护。
- 替代声明未指定任何 type_parameter_constraints_clause。 相反,约束继承自重写的基方法。 替代方法中类型参数的约束可以替换为继承约束中的类型参数。 这可能会导致显式指定时无效的约束,例如值类型或密封类型。
示例:下面演示重写规则如何适用于泛型类:
abstract class C<T> { public virtual T F() {...} public virtual C<T> G() {...} public virtual void H(C<T> x) {...} } class D : C<string> { public override string F() {...} // Ok public override C<string> G() {...} // Ok public override void H(C<T> x) {...} // Error, should be C<string> } class E<T,U> : C<U> { public override U F() {...} // Ok public override C<U> G() {...} // Ok public override void H(C<T> x) {...} // Error, should be C<U> }
end 示例
重写声明可以使用 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
。end 示例
只有包含 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
并报告警告,因为声明不包含新的修饰符。end 示例
示例:在以下代码中
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
。end 示例
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
。end 示例
15.6.7 抽象方法
当实例方法声明包含 abstract
修饰符时,该方法称为 抽象方法。 虽然抽象方法隐式也是虚拟方法,但它不能有修饰符 virtual
。
抽象方法声明引入了新的虚拟方法,但不提供该方法的实现。 相反,非抽象派生类需要通过重写该方法来提供自己的实现。 由于抽象方法不提供实际实现,因此抽象方法的方法正文仅包含分号。
抽象方法声明仅在抽象类中允许(§15.2.2.2.2)。
示例:在以下代码中
public abstract class Shape { public abstract void Paint(Graphics g, Rectangle r); } public class Ellipse : Shape { public override void Paint(Graphics g, Rectangle r) => g.DrawEllipse(r); } public class Box : Shape { public override void Paint(Graphics g, Rectangle r) => g.DrawRect(r); }
该
Shape
类定义可自行绘制的几何形状对象的抽象概念。 该方法Paint
是抽象的,因为没有有意义的默认实现。 这些Ellipse
和Box
类是具体的Shape
实现。 由于这些类是非抽象类,因此必须重写Paint
该方法并提供实际实现。end 示例
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()
编译时错误,因为它引用了抽象方法。end 示例
允许抽象方法声明重写虚拟方法。 这允许抽象类强制在派生类中重新实现方法,并使该方法的原始实现不可用。
示例:在以下代码中
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
重写抽象方法以提供自己的实现。end 示例
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); }
end 示例
15.6.9 分部方法
当方法声明包含 partial
修饰符时,该方法据说是分 部方法。 分部方法只能声明为分部类型的成员(§15.2.7),并且受到许多限制。
分部方法可以在类型声明的一部分定义,并在另一部分实现。 实现是可选的;如果没有部件实现分部方法,则分部方法声明及其所有调用都将从由部件组合生成的类型声明中删除。
分部方法不应定义访问修饰符;它们是隐式私有的。 其返回类型应为 void
,其参数不应为输出参数。 仅当标识符出现在关键字前面时,标识符部分才会在方法声明中识别为上下文关键字(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) {} }
无效,因为调用使用实现的参数名称,而不是定义分部方法声明。
end note
如果分部类型声明没有任何部分包含给定分部方法的实现声明,则调用该声明的任何表达式语句都只是从组合类型声明中删除。 因此,调用表达式(包括任何子表达式)在运行时不起作用。 分部方法本身也会被删除,并且不会是组合类型声明的成员。
如果给定分部方法存在实现声明,则保留分部方法的调用。 分部方法产生类似于实现分部方法声明的方法声明,但以下情况除外:
partial
不包括修饰符。生成的方法声明中的属性是定义和实现分部方法声明的组合属性(按未指定的顺序)。 不会删除重复项。
生成的方法声明的参数的属性是定义和未指定顺序实现分部方法声明的相应参数的组合属性。 不会删除重复项。
如果为分部方法 M
提供了定义声明而不是实现声明,则适用以下限制:
分部方法可用于允许类型声明的一部分自定义另一部分的行为,例如工具生成的部分。 请考虑以下分部类声明:
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; } }
end 示例
扩展方法是常规静态方法。 此外,如果其封闭静态类在范围内,可以使用接收器表达式作为第一个参数,使用实例方法调用语法(§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)); } } }
end 示例
15.6.11 方法正文
方法声明的方法正文由块体、表达式正文或分号组成。
抽象和外部方法声明不提供方法实现,因此其方法主体只包含分号。 对于任何其他方法,方法正文是一个块(§13.3),其中包含调用该方法时要执行的语句。
,或者该方法是异步的,并且返回类型为 (void
)。 否则,非异步方法的有效返回类型是其返回类型,具有返回类型的异步方法的有效返回类型 «TaskType»<T>
(§15.15.1) 为 T
。
当方法的有效返回类型是 void
且该方法具有块正文时, return
块中的语句(§13.10.5)不应指定表达式。 如果 void 方法块的执行正常完成(即控制流离方法正文的末尾),该方法只会返回其调用方。
当方法 void
的有效返回类型是且该方法具有表达式主体时,表达式 E
应为 statement_expression,并且该正文与窗体 { E; }
的块体完全等效。
对于返回值方法(§15.6.1),该方法正文中的每个返回语句应指定可隐式转换为有效返回类型的表达式。
对于 returns-by-ref 方法(§15.6.1),该方法正文中的每个返回语句应指定其类型为有效返回类型的表达式,并且具有调用方上下文的 ref-safe-context(§9.7.2)。
对于 returns-by-value 和 returns-by-ref 方法,方法正文的终结点不可访问。 换句话说,不允许控制从方法正文的末尾流动。
示例:在以下代码中
class A { public int F() {} // Error, return value required public int G() { return 1; } public int H(bool b) { if (b) { return 1; } else { return 0; } } public int I(bool b) => b ? 1 : 0; }
返回值
F
的方法会导致编译时错误,因为控制可以流出方法正文的末尾。 和G
H
方法是正确的,因为所有可能的执行路径都以指定返回值的返回语句结尾。 方法I
正确,因为它的正文等效于只包含一个返回语句的块。end 示例
15.7 属性
15.7.1 常规
属性是一个成员,它提供对对象或类的特征的访问权限。 属性的示例包括字符串的长度、字体大小、窗口的标题以及客户的名称。 属性是字段的自然扩展 - 两者都是具有关联类型的命名成员,访问字段和属性的语法是相同的。 不过,与字段不同的是,属性不指明存储位置。 相反,属性包含访问器,用于指定在读取或写入属性值时要执行的语句。 因此,属性提供一种机制,用于将操作与对象或类特征的读取和写入相关联;此外,它们允许计算此类特征。
使用 property_declarations 声明属性:
property_declaration
: attributes? property_modifier* type member_name property_body
| attributes? property_modifier* ref_kind type member_name ref_property_body
;
property_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
property_body
: '{' accessor_declarations '}' property_initializer?
| '=>' expression ';'
;
property_initializer
: '=' variable_initializer ';'
;
ref_property_body
: '{' ref_get_accessor_declaration '}'
| '=>' 'ref' variable_reference ';'
;
unsafe_modifier (§23.2)仅在不安全的代码(§23)中可用。
有两种类型的 property_declaration:
- 第一个声明非 ref 值属性。 其值具有类型类型。 此类属性可以是可读属性和/或可写属性。
- 第二个声明 ref 值属性。 其值为类型类型的变量variable_reference(
readonly
)。 此类属性仅可读。
property_declaration可能包括一组属性(§22)和任何一种允许的声明辅助功能(§15.3.6)、(§15.3.5new
static
)、 (virtual
、§15.7.6)、 override
()、(§15.6.7、sealed
)和(abstract
)修饰符。
对于修饰符的有效组合,属性声明受方法声明(§15.6)相同的规则约束。
member_name(§15.6.1)指定属性的名称。 除非该属性是显式接口成员实现,否则member_name只是标识符。 对于显式接口成员实现(§18.6.2),member_name由interface_type后跟“”和.
组成。
属性 的类型 应至少与属性本身(§7.5.5.5)一样可访问。
property_body可能包括语句正文或表达式正文。 在语句正文中,,声明属性的访问器({
)。 访问器指定与读取和写入属性关联的可执行语句。
在property_body由表达式后跟表达式的表达式=>
正文E
和分号完全等效于语句正文{ get { return E; } }
,因此只能用于指定只读属性,其中 get 访问器的结果由单个表达式提供。
只能为自动实现的属性(§15.7.4)提供property_initializer,并导致使用表达式给出的值初始化此类属性的基础字段。
ref_property_body可能由语句正文或表达式正文组成。 在语句正文中, get_accessor_declaration 声明属性的 get 访问器(§15.7.3)。 访问器指定与读取属性关联的可执行语句。
在ref_property_body由后接=>
的表达式正文ref
中,和分号与语句正文V
完全等效。
注意:尽管用于访问属性的语法与字段的语法相同,但不会将属性分类为变量。 因此,除非属性是 ref 值,
in
因此返回变量引用(out
),否则无法将属性作为ref
或自变量传递。 end note
当属性声明包含 extern
修饰符时,该属性据说是外部 属性。 由于外部属性声明不提供实际实现,因此其accessor_declarations中的每个accessor_body应为分号。
15.7.2 静态和实例属性
当属性声明包含 static
修饰符时,该属性据说是静态 属性。 当不存在 static
修饰符时,该属性称为 实例属性。
静态属性不与特定实例关联,它是在静态属性的访问器中引用 this
的编译时错误。
实例属性与类的给定实例相关联,该实例可在该属性的访问器中作为 (this
) 进行访问。
静态成员和实例成员之间的差异在 §15.3.8 中进一步讨论。
15.7.3 访问器
注意:此子句适用于属性(§15.7)和索引器(§15.9)。 在读取索引器替换属性/属性的索引器/索引器时,该子句以属性/属性编写,并查阅 §15.9.2 中给出的属性和索引器之间的差异列表。 end note
属性 accessor_declarations 指定与写入和/或读取该属性关联的可执行语句。
accessor_declarations
: get_accessor_declaration set_accessor_declaration?
| set_accessor_declaration get_accessor_declaration?
;
get_accessor_declaration
: attributes? accessor_modifier? 'get' accessor_body
;
set_accessor_declaration
: attributes? accessor_modifier? 'set' accessor_body
;
accessor_modifier
: 'protected'
| 'internal'
| 'private'
| 'protected' 'internal'
| 'internal' 'protected'
| 'protected' 'private'
| 'private' 'protected'
;
accessor_body
: block
| '=>' expression ';'
| ';'
;
ref_get_accessor_declaration
: attributes? accessor_modifier? 'get' ref_accessor_body
;
ref_accessor_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
accessor_declarations由get_accessor_declaration、set_accessor_declaration或两者组成。 每个访问器声明由可选属性、可选 accessor_modifier、标记 get
或 set
后跟 accessor_body组成。
对于 ref-valued 属性, ref_get_accessor_declaration 包含可选属性、可选 accessor_modifier、标记 get
,后跟 ref_accessor_body。
accessor_modifier的使用受以下限制的约束:
- 不应在接口或显式接口成员实现中使用accessor_modifier。
- 对于没有
override
修饰符的属性或索引器, 仅当属性或索引器同时具有 get 和 set 访问器时,才允许accessor_modifier ,然后仅允许在这些访问器之一上。 - 对于包含
override
修饰符的属性或索引器,访问器应与 被重写的访问器accessor_modifier(如果有)。 - accessor_modifier应声明比属性或索引器本身的声明可访问性更严格的可访问性。 确切地说:
- 如果属性或索引器具有声明的可访问性,则由accessor_modifier声明的可访问性
public
可以是、private protected
、protected internal
或internal
protected
。private
- 如果属性或索引器具有声明的可访问性,则由accessor_modifier声明的可访问性
protected internal
可以是、private protected
、protected private
或internal
protected
。private
- 如果属性或索引器具有声明的可访问性,则
internal
由accessor_modifierprotected
的可访问性应为private protected
或。private
- 如果属性或索引器具有声明的可访问性,则由accessor_modifier声明的可访问性
private protected
应为。private
- 如果属性或索引器具有声明的可访问性
private
, 则不能使用accessor_modifier 。
- 如果属性或索引器具有声明的可访问性,则由accessor_modifier声明的可访问性
对于 abstract
非 extern
ref 值属性,指定的每个访问器的任何 accessor_body 只是分号。 非抽象的非 extern 属性(但不是索引器)也可能为所有 访问器指定accessor_body 为分号,在这种情况下,它是 自动实现的属性 (§15.7.4)。 自动实现的属性至少应具有 get 访问器。 对于任何其他非抽象的非外部属性的访问器, accessor_body 为:
- 一个 块 ,指定要在调用相应访问器时执行的语句;或
- 一个表达式主体,由
=>
后跟表达式和分号组成,并表示在调用相应的访问器时要执行的单个表达式。
对于 abstract
和 extern
ref 值属性, ref_accessor_body 只是一个分号。 对于任何其他非抽象的非 extern 属性的访问器, ref_accessor_body 为:
- 一个 块 ,指定要在调用 get 访问器时执行的语句;或
- 表达式正文,
=>
后跟ref
variable_reference和分号。 调用 get 访问器时,将计算变量引用。
非 ref 值属性的 get 访问器对应于具有属性类型的返回值的无参数方法。 除赋值的目标外,在表达式中引用此类属性时,将调用 get 访问器来计算属性的值(§12.2.2)。
非 ref 值属性的 get 访问器的主体应符合 §15.6.11 中所述的值返回方法的规则。 具体而言,get 访问器主体中的所有 return
语句应指定可隐式转换为属性类型的表达式。 此外,不能访问 get 访问器的终结点。
ref-valued 属性的 get 访问器对应于一个无参数方法,该方法的返回值 variable_reference 属性类型的变量。 在表达式中引用此类属性时,将调用 get 访问器来计算 该属性的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 } }
end 示例
ref 值属性的 get 访问器的主体应符合 §15.6.11 中所述的 ref 值方法的规则。
set 访问器对应于具有属性类型和返回类型的 void
单个值参数的方法。 set 访问器的隐式参数始终命名 value
。 当将属性引用为赋值(§12.21++
、–-
)时,将调用 set 访问器,并提供新值(§12.21.2)。 set 访问器的正文应符合 §15.6.11void
方法的规则。 具体而言,不允许 set 访问器正文中的 return 语句指定表达式。 由于 set 访问器隐式具有一 value
个名为的参数,因此对于 set 访问器中的局部变量或常量声明具有该名称,这是一个编译时错误。
根据 get 和 set 访问器是否存在,属性分类如下:
- 一个包括 get 访问器和 set 访问器的属性据说是一个读写属性。
- 仅具有 get 访问器的属性据说是只读属性。 只读属性是赋值的目标的编译时错误。
- 仅具有 set 访问器的属性据说是一个 仅写属性。 除赋值的目标外,在表达式中引用仅写属性是编译时错误。
注意:前和后缀
++
和--
运算符和复合赋值运算符不能应用于仅写属性,因为这些运算符在写入新操作数之前读取其操作数的旧值。 end note
示例:在以下代码中
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 访问器。
end 示例
属性的 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; } }
不声明单个读写属性。 相反,它声明两个具有相同名称的属性,一个只读和一个只读。 由于在同一类中声明的两个成员不能具有相同的名称,该示例会导致发生编译时错误。
end 示例
当派生类按与继承属性相同的名称声明属性时,派生属性将隐藏与读取和写入相关的继承属性。
示例:在以下代码中
class A { public int P { set {...} } } class B : A { public new int P { get {...} } }
P
在B
读取和写入方面隐藏P
属性A
的属性。 因此,在语句中B b = new B(); b.P = 1; // Error, B.P is read-only ((A)b).P = 1; // Ok, reference to A.P
赋值会导致
b.P
报告编译时错误,因为只读P
属性隐藏B
P
写入属性。A
但是,请注意,强制转换可用于访问隐藏P
属性。end 示例
与公共字段不同,属性提供对象内部状态与其公共接口之间的分隔。
示例:请考虑以下代码,该代码使用
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
类进行这样的更改。end 示例
注意:通过属性公开状态并不一定比直接公开字段效率低。 具体而言,当属性是非虚拟的并且仅包含少量代码时,执行环境可能会将对访问器的调用替换为访问器的实际代码。 此过程称为 内联,它使属性访问与字段访问一样高效,但保留了属性的灵活性。 end note
示例:由于调用 get 访问器在概念上等效于读取字段的值,因此获取访问器的编程样式被视为具有可观察副作用的不良编程样式。 在示例中
class Counter { private int next; public int Next => next++; }
属性的值
Next
取决于以前访问该属性的次数。 因此,访问该属性会产生可观察的副作用,并且该属性应改为作为方法实现。get 访问器的“无副作用”约定并不意味着应始终编写 get 访问器以返回存储在字段中的值。 事实上,获取访问器通常通过访问多个字段或调用方法来计算属性的值。 但是,正确设计的 get 访问器不会执行任何导致对象状态可观察更改的操作。
end 示例
属性可用于延迟资源的初始化,直到首次引用资源的那一刻。
示例:
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
属性时,如 inConsole.Out.WriteLine("hello, world");
创建输出设备的基础
TextWriter
。 但是,如果应用程序没有引用In
和Error
属性,则不会为这些设备创建任何对象。end 示例
15.7.4 自动实现属性
自动实现的属性(或短属性)是一个非抽象、非 extern、非 ref 值属性,且只 使用分号accessor_bodys。 自动属性应具有 get 访问器,并且可以选择性地具有 set 访问器。
将属性指定为自动实现的属性时,隐藏后盾字段将自动可用于该属性,并且将实现访问器以读取和写入该后盾字段。 隐藏后盾字段不可访问,它只能通过自动实现的属性访问器读取和写入,即使在包含类型内也是如此。 如果 auto-property 没有 set 访问器,则支持字段被视为 readonly
(§15.5.3)。 与字段一 readonly
样,还可以在封闭类的构造函数的正文中分配只读自动属性。 此类赋值直接分配给属性的只读支持字段。
自动属性可以选择具有property_initializer,该variable_initializer(§17.7)直接应用于支持字段。
示例:
public class Point { public int X { get; set; } // Automatically implemented public int Y { get; set; } // Automatically implemented }
等效于以下声明:
public class Point { private int x; private int y; public int X { get { return x; } set { x = value; } } public int Y { get { return y; } set { y = value; } } }
end 示例
示例:在以下示例中
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; } }
只读字段的赋值有效,因为它们发生在构造函数中。
end 示例
尽管支持字段处于隐藏状态,但该字段可能通过自动实现的属性 的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; } } }
end 示例
15.7.5 辅助功能
如果访问器具有accessor_modifier,则访问器的辅助功能域(§7.5.3)使用accessor_modifier的声明可访问性确定。 如果访问器没有 accessor_modifier,则访问器的可访问性域取决于属性或索引器的声明可访问性。
存在accessor_modifier永远不会影响成员查找(§12.5)或重载分辨率(§12.6.4)。 无论访问上下文如何,属性或索引器上的修饰符始终确定绑定到哪个属性或索引器。
选择特定的非 ref 值属性或非 ref 值索引器后,涉及的特定访问器的辅助功能域用于确定该用法是否有效:
- 如果用法作为值(§12.2.2),则 get 访问器应存在并可访问。
- 如果用法是简单赋值的目标(§12.21.2),则设置访问器应存在并可访问。
- 如果用法是复合赋值的目标(§12.21.4),或者作为或
++
运算符的目标--
(§12.8.16,§12.9.6),则 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 } }
end 示例
一旦选择了特定的引用值属性或引用值索引器(无论是作为值、简单赋值的目标还是复合赋值的目标),所涉及的 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 } }
end 示例
15.7.6 虚拟、密封、替代和抽象访问器
注意:此子句适用于属性(§15.7)和索引器(§15.9)。 在读取索引器替换属性/属性的索引器/索引器时,该子句以属性/属性编写,并查阅 §15.9.2 中给出的属性和索引器之间的差异列表。 end note
虚拟属性声明指定属性的访问器是虚拟的。 修饰 virtual
符适用于属性的所有非专用访问器。 当虚拟属性的访问器具有private
accessor_modifier时,专用访问器隐式不是虚拟的。
抽象属性声明指定属性的访问器是虚拟的,但不提供访问器的实际实现。 相反,非抽象派生类需要通过重写属性为访问器提供自己的实现。 由于抽象属性声明的访问器不提供实际实现,因此其 accessor_body 只包含分号。 抽象属性不应具有 private
访问器。
包含 abstract
和 override
修饰符的属性声明指定属性是抽象的,并重写基属性。 此类属性的访问器也是抽象的。
抽象属性声明仅在抽象类中允许(§15.2.2.2.2)。 可以通过包含指定 override
指令的属性声明,在派生类中重写继承的虚拟属性的访问器。 这称为 重写属性声明。 重写属性声明不声明新属性。 相反,它只是专门介绍现有虚拟属性的访问器的实现。
重写声明和重写的基属性需要具有相同声明的可访问性。 换句话说,重写声明不应更改基属性的可访问性。 但是,如果重写的基属性在内部受到保护,并且它在包含重写声明的程序集之外的程序集中声明,则重写声明的声明可访问性应受到保护。 如果继承的属性只有一个访问器(即,如果继承的属性是只读的或只写的),则重写属性应仅包含该访问器。 如果继承的属性包括两个访问器(即,如果继承的属性是读写的),则重写属性可以包括单个访问器或两个访问器。 重写类型和继承属性的类型之间应存在标识转换。
重写属性声明可能包括 sealed
修饰符。 使用此修饰符可防止派生类进一步重写属性。 密封属性的访问器也密封。
除了声明和调用语法的差异外,虚拟、密封、重写和抽象访问器的行为与虚拟、密封、重写和抽象方法完全相同。 具体而言,§15.6.4、§15.6.5、§15.6.6 和 §15.6.7 中所述的规则适用,就像访问器是相应表单的方法一样:
- get 访问器对应于具有属性类型的返回值和与包含属性相同的修饰符的无参数方法。
- set 访问器对应于一个方法,该方法具有属性类型、void 返回类型以及与包含属性相同的修饰符。
示例:在以下代码中
abstract class A { int y; public virtual int X { get => 0; } public virtual int Y { get => y; set => y = value; } public abstract int Z { get; set; } }
X
是虚拟只读属性,Y
是虚拟读写属性,是Z
抽象的读写属性。 因为Z
是抽象的,因此应将包含类 A 声明为抽象类。派生自
A
的类如下所示:class B : A { int z; public override int X { get => base.X + 1; } public override int Y { set => base.Y = value < 0 ? 0: value; } public override int Z { get => z; set => z = value; } }
在这里,声明
X
Y
和Z
重写属性声明。 每个属性声明都与相应继承属性的辅助功能修饰符、类型和名称完全匹配。 使用基关键字访问继承访问器的 get 访问器和X
set 访问器Y
。 重写这两个Z
抽象访问器的声明-因此,没有未完成abstract
的函数成员B
,并允许B
成为非抽象类。end 示例
当属性声明为替代时,重写代码应可以访问任何重写访问器。 此外,属性或索引器本身和访问器的声明可访问性应与重写成员和访问器的可访问性相匹配。
示例:
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 } }
end 示例
15.8 事件
15.8.1 常规
借助事件成员,类或对象可以提供通知。 客户端可以通过提供事件处理程序来附加事件的可执行代码。
使用 event_declarations 声明事件:
event_declaration
: attributes? event_modifier* 'event' type variable_declarators ';'
| attributes? event_modifier* 'event' type member_name
'{' event_accessor_declarations '}'
;
event_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
event_accessor_declarations
: add_accessor_declaration remove_accessor_declaration
| remove_accessor_declaration add_accessor_declaration
;
add_accessor_declaration
: attributes? 'add' block
;
remove_accessor_declaration
: attributes? 'remove' block
;
unsafe_modifier (§23.2)仅在不安全的代码(§23)中可用。
event_declaration可能包括一组属性(§22)和任何一种允许的声明辅助功能(§15.3.6)、(15.3.5)、new
(§15.6.3、§15.6.3static
) virtual
、 (override
)、 ()、 sealed
(、abstract
)和(§15.6.8)修饰符。extern
对于修饰符的有效组合,事件声明受方法声明(§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包含修饰符和event_accessor_declaration是编译时错误abstract
。
当事件声明包含 extern
修饰符时,事件据说是外部 事件。 由于外部事件声明不提供实际实现,因此 extern
它包含修饰符和 event_accessor_declaration是错误的。
对于包含abstract
variable_initializer的事件声明variable_declaratorexternal
事件可用作左操作数 +=
和 -=
运算符。 这些运算符分别用于将事件处理程序附加到或从事件中删除事件处理程序,以及事件的访问修饰符控制允许此类操作的上下文。
由声明该事件的类型之外的代码允许对事件执行的唯一操作是 +=
和 -=
。 因此,尽管此类代码可以添加和删除事件的处理程序,但它无法直接获取或修改事件处理程序的基础列表。
在窗体x += y
的操作中,或者x –= y
,如果x
某个事件,则操作的结果具有类型void
(§12.21.5)(而不是具有x
x
赋值后的值,对于非事件类型定义的其他+=
和-=
运算符)。 这可以防止外部代码间接检查事件的基础委托。
示例:以下示例演示如何将事件处理程序附加到类的
Button
实例:public delegate void EventHandler(object sender, EventArgs e); public class Button : Control { public event EventHandler Click; } public class LoginDialog : Form { Button okButton; Button cancelButton; public LoginDialog() { okButton = new Button(...); okButton.Click += new EventHandler(OkButtonClick); cancelButton = new Button(...); cancelButton.Click += new EventHandler(CancelButtonClick); } void OkButtonClick(object sender, EventArgs e) { // Handle okButton.Click event } void CancelButtonClick(object sender, EventArgs e) { // Handle cancelButton.Click event } }
在这里,
LoginDialog
实例构造函数创建两Button
个实例,并将事件处理程序附加到Click
事件。end 示例
15.8.2 类似字段的事件
在包含事件声明的类或结构的程序文本中,某些事件可以像字段一样使用。 若要以这种方式使用,事件不得为抽象或外展,不得明确包括 event_accessor_declaration。 此类事件可在允许字段的任何上下文中使用。 该字段包含一个委托(§20),该委托引用已添加到事件的事件处理程序列表。 如果未添加事件处理程序,则字段包含 null
。
示例:在以下代码中
public delegate void EventHandler(object sender, EventArgs e); public class Button : Control { public event EventHandler Click; protected void OnClick(EventArgs e) { EventHandler handler = Click; if (handler != null) { handler(this, e); } } public void Reset() => Click = null; }
Click
用作类中的Button
字段。 如示例所示,可以在委托调用表达式中检查、修改和使用字段。OnClick
类中的Button
方法“引发”Click
事件。 引发事件的概念恰恰等同于调用由事件表示的委托,因此,没有用于引发事件的特殊语言构造。 请注意,委托调用前面有一个检查,确保委托为非 null,并且在本地副本上进行了检查以确保线程安全。在类的
Button
声明之外,Click
成员只能在左侧+=
使用,并且–=
运算符也可用,如中所示b.Click += new EventHandler(...);
将委托追加到事件的调用列表
Click
,以及Click –= new EventHandler(...);
从事件的调用列表中
Click
删除委托。end 示例
编译类似字段的事件时,编译器应自动创建存储来保存委托,并应为事件创建访问器,以向委托字段添加或删除事件处理程序。 添加和删除操作是线程安全的,在保存实例事件的包含对象的锁(§13.13System.Type
)。
注意:因此,窗体的实例事件声明:
class X { public event D Ev; }
应编译为等效的内容:
class X { private D __Ev; // field to hold the delegate public event D Ev { add { /* Add the delegate in a thread safe way */ } remove { /* Remove the delegate in a thread safe way */ } } }
在类
X
中,对Ev
左侧的+=
引用和–=
运算符会导致调用添加和删除访问器。 所有其他引用Ev
都编译为改为引用隐藏字段__Ev
(§12.8.7)。 名称“__Ev
”是任意的;隐藏字段可能具有任何名称或根本没有名称。end note
15.8.3 事件访问器
注意:事件声明通常省略 event_accessor_declarations,如上面的示例所示
Button
。 例如,如果每个事件的存储成本不能接受,则可能会包含这些字段。 在这种情况下,类可以包含 event_accessor_declarations,并使用专用机制来存储事件处理程序列表。 end note
事件的 event_accessor_declarations 指定与添加和删除事件处理程序关联的可执行语句。
访问器声明由add_accessor_declaration和remove_accessor_declaration组成。 每个访问器声明由标记添加或删除后跟块组成。 与add_accessor_declaration关联的块指定要在添加事件处理程序时执行的语句,并且与remove_accessor_declaration关联的块指定要在删除事件处理程序时执行的语句。
每个 add_accessor_declaration 和 remove_accessor_declaration 对应于事件类型的单个值参数和 void
返回类型的方法。 事件访问器的隐式参数命名 value
。 在事件分配中使用事件时,将使用相应的事件访问器。 具体而言,如果赋值运算符是 +=
使用 add 访问器,则使用赋值运算符 –=
,然后使用 remove 访问器。 在任一情况下,赋值运算符的右操作数用作事件访问器的参数。 add_accessor_declaration或remove_accessor_declaration块应符合 §15.6.9 中所述方法的规则。void
具体而言, 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 委托值与键相关联,因此未经处理的事件不会占用任何存储费用。end 示例
15.8.4 静态事件和实例事件
当事件声明包含 static
修饰符时,事件据说是静态 事件。 当不存在 static
修饰符时,事件将称为实例 事件。
静态事件不与特定实例关联,它是在静态事件的访问器中引用 this
的编译时错误。
实例事件与类的给定实例相关联,此实例可在该事件的访问器中作为 (this
) 进行访问。
静态成员和实例成员之间的差异在 §15.3.8 中进一步讨论。
15.8.5 虚拟、密封、重写和抽象访问器
虚拟事件声明指定该事件的访问器是虚拟的。 修饰 virtual
符适用于事件的两个访问器。
抽象事件声明指定事件的访问器是虚拟的,但不提供访问器的实际实现。 相反,非抽象派生类需要通过重写事件为访问器提供自己的实现。 由于抽象事件声明的访问器不提供实际实现,因此它不应提供 event_accessor_declaration。
包含 abstract
和 override
修饰符的事件声明指定事件是抽象的,并重写基事件。 此类事件的访问器也是抽象的。
抽象事件声明仅在抽象类中允许(§15.2.2.2.2)。
可以通过包含指定 override
修饰符的事件声明,在派生类中重写继承的虚拟事件的访问器。 这称为 重写事件声明。 重写事件声明不声明新事件。 相反,它只是专门介绍现有虚拟事件的访问器的实现。
重写事件声明应指定与重写事件完全相同的辅助功能修饰符和名称,在重写和重写事件的类型之间应存在标识转换,并且添加和删除访问器应在声明中指定。
重写事件声明可以包含 sealed
修饰符。 使用 this
修饰符可防止派生类进一步重写事件。 密封事件的访问器也会密封。
重写事件声明 new
包含修饰符是编译时错误。
除了声明和调用语法的差异外,虚拟、密封、重写和抽象访问器的行为与虚拟、密封、重写和抽象方法完全相同。 具体而言,§15.6.4、§15.6.5、§15.6.6 和 §15.6.7 中所述的规则适用,就像访问器是相应表单的方法一样。 每个访问器对应于一个方法,该方法具有事件类型的单个值参数、 void
返回类型,以及与包含事件相同的修饰符。
15.9 索引器
15.9.1 常规
索引器是一个成员,它使对象能够以与数组相同的方式编制索引。 索引器使用 indexer_declarations 声明:
indexer_declaration
: attributes? indexer_modifier* indexer_declarator indexer_body
| attributes? indexer_modifier* ref_kind indexer_declarator ref_indexer_body
;
indexer_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
indexer_declarator
: type 'this' '[' parameter_list ']'
| type interface_type '.' 'this' '[' parameter_list ']'
;
indexer_body
: '{' accessor_declarations '}'
| '=>' expression ';'
;
ref_indexer_body
: '{' ref_get_accessor_declaration '}'
| '=>' 'ref' variable_reference ';'
;
unsafe_modifier (§23.2)仅在不安全的代码(§23)中可用。
有两种类型的 indexer_declaration:
- 第一个声明非 ref 值索引器。 其值具有类型类型。 此类索引器可以是可读的和/或可写的。
- 第二个声明 ref 值索引器。 其值为类型类型的变量variable_reference(
readonly
)。 此类索引器仅可读。
indexer_declaration可能包括一组属性(§22)和任何一种允许的声明辅助功能(§15.3.6)、(§15.3.5)、new
(§15.3.5virtual
、 (override
)、 (sealed
)、 (abstract
)和(extern
)修饰符。
对于修饰符的有效组合,索引器声明受方法声明(§15.6)相同的规则,但有一个例外是 static
不允许对索引器声明使用修饰符。
索引器声明的类型指定声明引入的索引器的元素类型。
注意:由于索引器旨在用于数组类似元素的上下文,因此为数组定义的术语 元素类型 也与索引器一起使用。 end note
除非索引器是显式接口成员实现,否则类型后跟关键字this
。 对于显式接口成员实现,该 类型 后跟 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
中,和分号与语句正文V
完全等效。
注意:即使访问索引器元素的语法与数组元素的语法相同,索引器元素也不会归类为变量。 因此,除非索引器为 ref 值,否则无法将索引器元素作为
in
out
参数ref
传递,因此返回引用 (§9.7)。 end note
索引 器的parameter_list 定义索引器的签名(§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
位),但它允许与 abool[]
相同的操作。以下
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; } } }
end 示例
15.9.2 索引器和属性差异
索引器和属性在概念上非常相似,但在以下方面有所不同:
- 属性由其名称标识,而索引器由其签名标识。
- 可通过simple_name(§12.8.4)或member_access(§12.8.7)访问属性,而索引器元素通过element_access(§12.8.12.3)进行访问。
- 属性可以是静态成员,而索引器始终是实例成员。
- 属性的 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 (§23.2)仅在不安全的代码(§23)中可用。
注意:前缀逻辑求反(§12.9.4)和后缀 null-forgiving 运算符(§12.8.9),而用同一词法标记(!
)表示,则不同。 后者不是可重载运算符。 end note
可重载运算符有三类:一元运算符(§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
修饰符。 - 运算符的参数(s)不得具有除其他修饰符以外的
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
一元运算符需要成对声明。 如果类声明其中一个运算符而不声明另一个运算符,则会发生编译时错误。 §12.24true
进一步介绍了这些false
和运算符。
示例:以下示例演示整数向量类运算符++ 的实现和后续用法:
public class IntVector { public IntVector(int length) {...} public int Length { get { ... } } // Read-only property public int this[int index] { get { ... } set { ... } } // Read-write indexer public static IntVector operator++(IntVector iv) { IntVector temp = new IntVector(iv.Length); for (int i = 0; i < iv.Length; i++) { temp[i] = iv[i] + 1; } return temp; } } class Test { static void Main() { IntVector iv1 = new IntVector(4); // Vector of 4 x 0 IntVector iv2; iv2 = iv1++; // iv2 contains 4 x 0, iv1 contains 4 x 1 iv2 = ++iv1; // iv2 contains 4 x 2, iv1 contains 4 x 2 } }
请注意运算符方法如何返回通过向操作数添加 1 生成的值,就像后缀递增和递减运算符(§12.8.16)和前缀递增和递减运算符 (§12.9.6) 一样。 与C++不同,此方法不应直接修改其操作数的值,因为这将违反后缀递增运算符(§12.8.16)的标准语义。
end 示例
15.10.3 二进制运算符
以下规则适用于二进制运算符声明,其中 T
表示包含运算符声明的类或结构的实例类型:
- 二进制非移位运算符应采用两个参数,其中至少一个参数应具有类型
T
,T?
也可以返回任何类型。 - 二进制
<<
或>>
运算符(§12.11)应采用两个参数,其中第一个参数应具有类型T
或T?
,第二个参数应具有类型int
或int?
,并且可以返回任何类型。
二进制运算符的签名由运算符标记(+
、、-
*
/
%
&
|
^
<<
>>
、==
!=
>
<
或>=
<=
)和两个参数的类型组成。 返回类型和参数名称不是二进制运算符签名的一部分。
某些二进制运算符需要成对声明。 对于对的任一运算符的每个声明,应有对的其他运算符的匹配声明。 如果标识转换在其返回类型与相应的参数类型之间存在,则两个运算符声明匹配。 以下运算符需要成对声明:
- 运算符
==
和运算符!=
- 运算符
>
和运算符<
- 运算符
>=
和运算符<=
15.10.4 转换运算符
转换运算符声明引入了用户定义的转换(§10.5),该转换可增强预定义的隐式转换和显式转换。
包含关键字的 implicit
转换运算符声明引入了用户定义的隐式转换。 隐式转换可能发生在各种情况下,包括函数成员调用、强制转换表达式和赋值。 这在 §10.2 中进一步介绍。
包含关键字的 explicit
转换运算符声明引入了用户定义的显式转换。 显式转换可以在强制转换表达式中发生,并在 §10.3 中进一步介绍。
转换运算符从源类型(由转换运算符的参数类型指示)转换为目标类型,由转换运算符的返回类型指示。
对于给定的源类型和S
目标类型,如果T
或S
为可以为 null 的值类型T
,则让S₀
和T₀
引用其基础类型;否则,S₀
它们T₀
分别等于S
和T
分别。 仅当以下所有内容均为 true 时,才允许类或结构声明从源类型 S
转换为目标类型 T
:
S₀
和T₀
不同类型的。S₀
T₀
或者是包含运算符声明的类或结构的实例类型。既
S₀
T₀
不是interface_type。排除用户定义的转换,转换不存在于
S
或从T
中T
转换S
。
对于这些规则,与这些S
规则关联的T
任何类型参数或被视为与其他类型的没有继承关系的唯一类型,并且忽略对这些类型参数的任何约束。
示例:在以下各项中:
class C<T> {...} class D<T> : C<T> { public static implicit operator C<int>(D<T> value) {...} // Ok public static implicit operator C<string>(D<T> value) {...} // Ok public static implicit operator C<T>(D<T> value) {...} // Error }
前两个运算符声明是允许的,并且
T
int
string
,分别被视为没有关系的唯一类型。 但是,第三个运算符是一个错误,因为C<T>
是基类 。D<T>
end 示例
从第二个规则中,转换运算符应转换为或从声明运算符的类或结构类型进行转换。
示例:类或结构类型
C
可以定义从C
/int
向和从int
到C
的转换,但不能从int
中转换。bool
end 示例
无法直接重新定义预定义的转换。 因此,不允许转换运算符从或转换, object
因为隐式和显式转换已存在于所有其他类型之间 object
。 同样,转换的源类型和目标类型都不能是另一种转换的基类型,因为转换将已经存在。 但是,可以声明泛型类型的运算符,对于特定类型参数,可以指定已存在的转换作为预定义转换。
示例:
struct Convertible<T> { public static implicit operator Convertible<T>(T value) {...} public static explicit operator T(Convertible<T> value) {...} }
当类型被指定为类型
object
参数T
时,第二个运算符声明已存在的转换(隐式转换,因此也是从任何类型到类型对象的显式转换)。end 示例
如果两种类型之间存在预定义转换,则忽略这些类型之间的任何用户定义的转换。 具体而言:
对于所有类型,但 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 }
end 示例
不允许用户定义转换或从 interface_type转换。 具体而言,此限制可确保在转换为interface_type时不会发生用户定义的转换,并且仅当转换到interface_type时,转换object
才会成功。
转换运算符的签名由源类型和目标类型组成。 (这是返回类型参与签名的唯一成员形式。转换运算符的隐式或显式分类不是运算符签名的一部分。 因此,类或结构不能同时声明具有相同源和目标类型的隐式转换运算符和显式转换运算符。
注意:一般情况下,用户定义隐式转换的设计应永远不会引发异常,也不会丢失信息。 如果用户定义的转换可能会引发异常(例如,因为源参数范围不足)或信息丢失(如放弃高阶位),则应将该转换定义为显式转换。 end note
示例:在以下代码中
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
子集。end 示例
15.11 实例构造函数
15.11.1 常规
实例构造函数是实现初始化类实例所需执行的操作的成员。 实例构造函数使用 constructor_declarations 声明:
constructor_declaration
: attributes? constructor_modifier* constructor_declarator constructor_body
;
constructor_modifier
: 'public'
| 'protected'
| 'internal'
| 'private'
| 'extern'
| unsafe_modifier // unsafe code support
;
constructor_declarator
: identifier '(' parameter_list? ')' constructor_initializer?
;
constructor_initializer
: ':' 'base' '(' argument_list? ')'
| ':' 'this' '(' argument_list? ')'
;
constructor_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2)仅在不安全的代码(§23)中可用。
constructor_declaration可以包括一组属性(§22)、任何一种允许的声明辅助功能(§15.3.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与返回类型(§15.6.11)的实例方法void
块完全对应。
不继承实例构造函数。 因此,类没有在类中实际声明的构造函数以外的实例构造函数,但例外是,如果类不包含实例构造函数声明,则会自动提供默认实例构造函数(§15.11.5)。
实例构造函数由 object_creation_expression s(§12.8.17.2)和 constructor_initializers 调用。
15.11.2 构造函数初始值设定项
所有实例构造函数(类除外object
)都隐式包含对constructor_body前另一个实例构造函数的调用。 隐式调用的 构造函数由constructor_initializer确定:
- 表单
base(
(其中)
是可选的)会导致从直接基类调用实例构造函数。 使用argument_list和 §12.6.4 的重载解析规则选择该构造函数。 候选实例构造函数集由直接基类的所有可访问实例构造函数组成。 如果此集为空,或者无法识别单个最佳实例构造函数,则会发生编译时错误。 - 表单
this(
(其中)
是可选的)从同一类调用另一个实例构造函数。 使用argument_list和 §12.6.4 的重载解析规则选择构造函数。 候选实例构造函数集包含类本身中声明的所有实例构造函数。 如果生成的一组适用的实例构造函数为空,或者无法识别单个最佳实例构造函数,则会发生编译时错误。 如果实例构造函数声明通过一个或多个构造函数初始值设定项链调用自身,则会发生编译时错误。
如果实例构造函数没有构造函数初始值设定项,则隐式提供表单 base()
的构造函数初始值设定项。
注意:因此,窗体的实例构造函数声明
C(...) {...}
与
C(...) : base() {...}
end note
实例构造函数声明的parameter_list给出的参数范围包括该声明的构造函数初始值设定项。 因此,允许构造函数初始值设定项访问构造函数的参数。
示例:
class A { public A(int x, int y) {} } class B: A { public B(int x, int y) : base(x + y, x - y) {} }
end 示例
实例构造函数初始值设定项无法访问正在创建的实例。 因此,在构造函数初始值设定项的参数表达式中引用此值是编译时错误,因为参数表达式通过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
0),因为直到基类构造函数返回之后才会执行要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; } }
end 示例
15.11.5 默认构造函数
如果类不包含实例构造函数声明,则会自动提供默认实例构造函数。 该默认构造函数只是调用直接基类的构造函数,就像它具有窗体 base()
的构造函数初始值设定项一样。 如果类是抽象的,则默认构造函数的声明辅助功能受到保护。 否则,默认构造函数的声明辅助功能是公共的。
注意:因此,默认构造函数始终为窗体
protected C(): base() {}
或
public C(): base() {}
其中
C
是类的名称。end note
如果重载解析无法确定基类构造函数初始值设定项的唯一最佳候选项,则会发生编译时错误。
示例:在以下代码中
class Message { object sender; string text; }
提供了默认构造函数,因为该类不包含实例构造函数声明。 因此,该示例与示例完全相同
class Message { object sender; string text; public Message() : base() {} }
end 示例
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 (§23.2)仅在不安全的代码(§23)中可用。
static_constructor_declaration可能包括一组属性(§22)和修饰符(extern
)。
static_constructor_declaration的标识符应命名声明静态构造函数的类。 如果指定了任何其他名称,则会发生编译时错误。
当静态构造函数声明包含 extern
修饰符时,静态构造函数据说是 外部静态构造函数。 由于外部静态构造函数声明不提供实际实现,因此其 static_constructor_body 由分号组成。 对于所有其他静态构造函数声明, static_constructor_body 由任一类型组成
- 一个 块,它指定要执行的语句以初始化类;或
- 表达式正文,后
=>
跟 表达式 和分号,并表示要执行的单个表达式以初始化类。
块或表达式正文的static_constructor_body与返回类型(§15.6.11)的静态方法void
的method_body完全对应。
静态构造函数不继承,不能直接调用。
关闭类的静态构造函数在给定的应用程序域中最多执行一次。 静态构造函数的执行由应用程序域中发生的以下事件中的第一个触发:
- 将创建类的实例。
- 引用类的任何静态成员。
如果类包含 Main
在其中开始执行的方法(§7.1),则调用该方法之前 Main
,该类的静态构造函数将执行。
若要初始化新的封闭类类型,请首先为该特定封闭类型创建一组新的静态字段(§15.5.2)。 每个静态字段都初始化为其默认值(§15.5.5)。 接下来,为这些静态字段执行静态字段初始值设定项(§15.5.6.2)。 最后,执行静态构造函数。
示例:示例
class Test { static void Main() { A.F(); B.F(); } } class A { static A() { Console.WriteLine("Init A"); } public static void F() { Console.WriteLine("A.F"); } } class B { static B() { Console.WriteLine("Init B"); } public static void F() { Console.WriteLine("B.F"); } }
必须生成输出:
Init A A.F Init B B.F
因为静态构造函数的执行
A
由调用A.F
触发,B
并且执行静态构造函数由调用B.F
触发。end 示例
可以构造循环依赖项,以允许以默认值状态观察具有变量初始值设定项的静态字段。
示例:示例
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。end 示例
由于静态构造函数对每个封闭构造类类型执行一次,因此对无法通过约束(§15.2.5)在编译时检查的类型参数强制实施运行时检查是一个方便的位置。
示例:以下类型使用静态构造函数强制类型参数为枚举:
class Gen<T> where T : struct { static Gen() { if (!typeof(T).IsEnum) { throw new ArgumentException("T must be an enum"); } } }
end 示例
15.13 终结器
注意:在此规范的早期版本中,现在称为“终结器”的称为“析构函数”。 经验表明,术语“析构函数”引起了混淆,并经常导致不正确的期望,尤其是对知道C++的程序员。 在C++中,析构函数以确定方式调用,而在 C# 中,终结器不是。 若要从 C# 获取确定行为,应使用
Dispose
。 end note
终结器是实现完成类实例所需的操作的成员。 使用finalizer_declaration声明终结器:
finalizer_declaration
: attributes? '~' identifier '(' ')' finalizer_body
| attributes? 'extern' unsafe_modifier? '~' identifier '(' ')'
finalizer_body
| attributes? unsafe_modifier 'extern'? '~' identifier '(' ')'
finalizer_body
;
finalizer_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2)仅在不安全的代码(§23)中可用。
finalizer_declaration可能包括一组属性(§22)。
finalizer_declarator的标识符应命名声明终结器所在的类。 如果指定了任何其他名称,则会发生编译时错误。
当终结器声明包含extern
修饰符时,终结器据说是外部终结器。 由于外部终结器声明不提供实际实现,因此其 finalizer_body 由分号组成。 对于所有其他终结器, finalizer_body 由任一类型组成
- 一个 块,它指定要执行的语句,以便完成类的实例。
- 或表达式正文,它由
=>
表达式和分号组成,并表示要执行的单个表达式,以便完成类的实例。
作为块或表达式正文的finalizer_body与返回类型(void
)的实例方法的method_body完全对应。
未继承终结器。 因此,类除了可能在该类中声明的终结器之外没有终结器。
注意:由于终结器不需要任何参数,因此无法重载它,因此类最多可以有一个终结器。 end note
终结器会自动调用,不能显式调用。 当任何代码不再可以使用该实例时,实例就有资格进行最终化。 实例的终结器执行在实例有资格完成(§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
因为继承链中的终结器按顺序调用,从大多数派生到最不派生。
end 示例
终结器是通过重写虚拟方法 Finalize
实现 System.Object
的。 不允许 C# 程序重写此方法或直接调用该方法(或重写)。
示例:例如,程序
class A { override protected void Finalize() {} // Error public void F() { this.Finalize(); // Error } }
包含两个错误。
end 示例
编译器的行为应当如同此方法及其替代根本不存在一样。
示例:因此,此程序:
class A { void Finalize() {} // Permitted }
有效且显示的方法隐藏
System.Object
方法Finalize
。end 示例
有关从终结器引发异常时的行为的讨论,请参阅 §21.4。
15.14 迭代器
15.14.1 常规
使用迭代器块(§13.3)实现的函数成员(§12.6)称为迭代器。
只要相应的函数成员的返回类型是枚举器接口(§15.14.2)或可枚举接口之一(§15.14.3),迭代器块就可以用作函数成员的主体。 它可能发生为 method_body、 operator_body 或 accessor_body,而事件、实例构造函数、静态构造函数和终结器不应作为迭代器实现。
使用迭代器块实现函数成员时,函数成员的参数列表是一个编译时错误,用于指定任何in
out
类型或参数或ref
参数ref struct
。
15.14.2 枚举器接口
枚举器接口是非泛型接口System.Collections.IEnumerator
和泛型接口System.Collections.Generic.IEnumerator<T>
的所有实例化。 为了简洁起见,在此子元素及其同级中,这些接口分别被引用为 IEnumerator
和 IEnumerator<T>
。
15.14.3 可枚举接口
可枚举接口是非泛型接口System.Collections.IEnumerable
和泛型接口System.Collections.Generic.IEnumerable<T>
的所有实例化。 为了简洁起见,在此子元素及其同级中,这些接口分别被引用为 IEnumerable
和 IEnumerable<T>
。
15.14.4 Yield 类型
迭代器生成一系列值(所有相同类型)。 此类型称为 迭代器的生成类型 。
- 返回或
IEnumerator
为IEnumerable
的迭代器的object
生成类型。 - 返回或
IEnumerator<T>
为IEnumerable<T>
的迭代器的T
生成类型。
15.14.5 枚举器对象
15.14.5.1 常规
使用迭代器块实现返回枚举器接口类型的函数成员时,调用函数成员不会立即在迭代器块中执行代码。 而是创建并返回枚举器对象。 此对象封装迭代器块中指定的代码,并在调用枚举器对象 MoveNext
的方法时在迭代器块中执行代码。 枚举器对象具有以下特征:
- 它实现
IEnumerator
并IEnumerator<T>
实现T
迭代器的收益率类型。 - 它实现
System.IDisposable
。 - 它使用参数值的副本(如果有)和传递给函数成员的实例值进行初始化。
- 它具有四个潜在状态,在运行、挂起和之后,最初处于之前状态。
枚举器对象通常是编译器生成的枚举器类的实例,该类封装迭代器块中的代码并实现枚举器接口,但可以实现其他实现方法。 如果枚举器类由编译器生成,该类将直接或间接嵌套在包含函数成员的类中,它将具有私有辅助功能,并且它将具有保留供编译器使用的名称(§6.4.3)。
枚举器对象可能实现的接口数可能多于上面指定的接口。
以下子项描述了枚举器对象提供的所需行为MoveNext
以及接口实现的成员Current
Dispose
。IEnumerator
IEnumerator<T>
枚举器对象不支持该方法 IEnumerator.Reset
。 调用此方法会导致 System.NotSupportedException
引发。
15.14.5.2 MoveNext 方法
MoveNext
枚举器对象的方法封装迭代器块的代码。 调用 MoveNext
该方法会在迭代器块中执行代码,并根据需要设置 Current
枚举器对象的属性。 调用时MoveNext
,所MoveNext
执行的精确操作取决于枚举器对象的状态:
- 如果枚举器对象 的状态为之前,则
MoveNext
调用:- 将状态更改为 “正在运行”。
- 将迭代器块的参数(包括
this
)初始化为初始化枚举器对象时保存的参数值和实例值。 - 从头开始执行迭代器块,直到执行中断(如下所述)。
- 如果枚举器对象的状态正在运行,则调用
MoveNext
的结果未指定。 - 如果枚举器对象的 状态已挂起,则调用 MoveNext:
- 将状态更改为 “正在运行”。
- 将所有局部变量和参数(包括
this
)的值还原到上次挂起迭代器块执行时保存的值。注意:自上一次调用
MoveNext
以来,这些变量引用的任何对象的内容都可能已更改。 end note - 继续执行迭代器块,紧跟在生成返回语句之后,该语句导致执行暂停,并继续执行,直到执行中断(如下所述)。
- 如果枚举器对象的状态在 之后,则
MoveNext
调用返回 false。
执行迭代器块时 MoveNext
,可以通过四种方式中断执行:通过 yield return
语句、 yield break
语句、遇到迭代器块的末尾,以及引发并传播出迭代器块的异常。
yield return
遇到语句时(§9.4.4.20):- 计算语句中给定的表达式,隐式转换为 yield 类型,并分配给
Current
枚举器对象的属性。 - 迭代器正文的执行已暂停。 保存所有局部变量和参数(包括
this
)的值,如此yield return
语句的位置一样。yield return
如果语句位于一个或多个try
块内,则此时不会执行关联的最终块。 - 枚举器对象的状态更改为 挂起。
- 该方法
MoveNext
返回true
其调用方,指示迭代成功推进到下一个值。
- 计算语句中给定的表达式,隐式转换为 yield 类型,并分配给
yield break
遇到语句时(§9.4.4.20):yield break
如果语句位于一个或多个try
块内,则执行关联的finally
块。- 枚举器对象的状态更改为 之后。
- 该方法
MoveNext
将返回到false
其调用方,指示迭代已完成。
- 遇到迭代器正文的末尾时:
- 枚举器对象的状态更改为 之后。
- 该方法
MoveNext
将返回到false
其调用方,指示迭代已完成。
- 引发异常并将其传播出迭代器块时:
- 迭代器正文中的相应
finally
块将由异常传播执行。 - 枚举器对象的状态更改为 之后。
- 异常传播继续向方法的
MoveNext
调用方传递。
- 迭代器正文中的相应
15.14.5.3 当前属性
枚举器对象的 Current
属性受 yield return
迭代器块中的语句影响。
当枚举器对象处于 挂起 状态时,值 Current
是上一次调用 MoveNext
所设置的值。 当枚举器对象处于 之前、 正在运行或 状态之后 时,访问 Current
的结果将未指定。
对于具有非生成类型的object
迭代器,通过枚举器对象的实现进行访问的结果对应于通过枚举器对象的Current
IEnumerable
实现进行访问Current
IEnumerator<T>
并将结果object
强制转换为 。
15.14.5.4 Dispose 方法
该方法 Dispose
用于通过将枚举器对象 引入后 状态来清理迭代。
- 如果枚举器对象的状态以前,则
Dispose
调用状态将更改为之后。 - 如果枚举器对象的状态正在运行,则调用
Dispose
的结果未指定。 - 如果枚举器对象的 状态已挂起,则
Dispose
调用:- 将状态更改为 “正在运行”。
- 执行任何最终块,就像上次执行的
yield return
语句是一个yield break
语句一样。 如果这会导致引发异常并将其传播出迭代器正文,枚举器对象的状态将设置为 之后 ,并将异常传播到方法的Dispose
调用方。 - 将状态更改为 之后。
- 如果枚举器对象的状态在 之后,则
Dispose
调用不会影响。
15.14.6 可枚举对象
15.14.6.1 常规
当使用迭代器块实现返回可枚举接口类型的函数成员时,调用函数成员不会立即在迭代器块中执行代码。 而是创建并返回可枚举对象。 可枚举对象 GetEnumerator
的方法返回一个枚举器对象,该枚举器对象封装迭代器块中指定的代码,并在调用枚举器对象 MoveNext
的方法时在迭代器块中执行代码。 可枚举对象具有以下特征:
- 它实现
IEnumerable
并IEnumerable<T>
实现T
迭代器的收益率类型。 - 它使用参数值的副本(如果有)和传递给函数成员的实例值进行初始化。
可枚举对象通常是编译器生成的可枚举类的实例,该类封装迭代器块中的代码并实现可枚举接口,但可以实现其他实现方法。 如果编译器生成可枚举类,该类将直接或间接嵌套在包含函数成员的类中,它将具有私有辅助功能,并且它将具有保留供编译器使用的名称(§6.4.3)。
可枚举对象可能实现的接口可能多于上面指定的接口。
注意:例如,可枚举对象也可以实现
IEnumerator
并IEnumerator<T>
启用它作为可枚举和枚举器。 通常,此类实现将从第一次调用GetEnumerator
返回其自己的实例(以保存分配)。 后续调用GetEnumerator
(如果有)将返回一个新的类实例(通常是同一类),以便对不同枚举器实例的调用不会相互影响。 即使前面的枚举器已经枚举超过序列末尾,它也无法返回同一实例,因为对已用尽枚举器的所有将来调用都必须引发异常。 end note
15.14.6.2 GetEnumerator 方法
可枚举对象提供方法的实现GetEnumerator
IEnumerable
和IEnumerable<T>
接口。 这两 GetEnumerator
种方法共享一个通用实现,该实现获取并返回一个可用的枚举器对象。 枚举器对象使用初始化可枚举对象时保存的参数值和实例值进行初始化,否则枚举器对象函数如 §15.14.5 中所述。
15.15 异步函数
15.15.1 常规
具有修饰符的方法(§15.6)或匿名函数(§12.19)称为async
。 一般情况下,术语 异步 用于描述具有 async
修饰符的任何类型的函数。
它是异步函数的参数列表的编译时错误,用于指定任何 in
、 out
或 ref
参数或类型的任何参数 ref struct
。
异步方法的return_type应为或void
任务类型。 对于生成结果值的异步方法,任务类型应为泛型。 对于不生成结果值的异步方法,任务类型不应为泛型。 此类类型在此规范 «TaskType»<T>
中分别称为和 «TaskType»
。 从中构造的标准库类型和类型是任务类型System.Threading.Tasks.Task
,以及通过属性System.Threading.Tasks.Task<TResult>
与任务生成器类型关联的类、结构或接口类型。System.Runtime.CompilerServices.AsyncMethodBuilderAttribute
此类类型在此规范 «TaskBuilderType»<T>
中称为和 «TaskBuilderType»
。 任务类型最多可以有一个类型参数,不能嵌套在泛型类型中。
返回任务类型的异步方法据说是任务返回。
任务类型在确切定义中可能有所不同,但从语言的角度来看,任务类型处于不完整、成功或出错的状态之一。 出错的任务记录相关的异常。 成功«TaskType»<T>
记录类型T
的结果。 任务类型是可等待的,因此任务可以是 await 表达式(§12.9.8)的操作数。
示例:任务类型
MyTask<T>
与任务生成器类型和MyTaskMethodBuilder<T>
awaiter 类型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() { ... } }
end 示例
任务生成器类型是对应于特定任务类型的类或结构类型(§15.15.2)。 任务生成器类型应与其相应任务类型的声明可访问性完全匹配。
注意: 如果声明
internal
任务类型,还必须在同一程序集中声明internal
并定义相应的生成器类型。 如果任务类型嵌套在另一个类型内,则任务浮点器类型也必须嵌套在同一类型中。 end note
异步函数可以通过 await 表达式(§12.9.8)在其正文中暂停计算。 稍后可以通过恢复委托在暂停 await 表达式时恢复评估。 恢复委托的类型 System.Action
,调用时,异步函数调用的计算将从等待表达式中恢复,该表达式会从它离开的位置恢复。 异步函数调用的当前调用方是原始调用方,如果函数调用从未挂起,或者恢复委托的最新调用方,则为原始调用方。
15.15.2 任务类型生成器模式
任务生成器类型最多可以有一个类型参数,不能嵌套在泛型类型中。 任务生成器类型应具有以下成员(对于非泛型任务生成器类型, SetResult
没有参数),具有声明 public
的可访问性:
class «TaskBuilderType»<T>
{
public static «TaskBuilderType»<T> Create();
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine;
public void SetStateMachine(IAsyncStateMachine stateMachine);
public void SetException(Exception exception);
public void SetResult(T result);
public void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine;
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine;
public «TaskType»<T> Task { get; }
}
编译器应生成使用 «TaskBuilderType» 实现暂停和恢复异步函数计算的语义的代码。 编译器按如下方式使用 «TaskBuilderType»:
«TaskBuilderType».Create()
调用 以创建在此列表中命名builder
的 «TaskBuilderType» 实例。builder.Start(ref stateMachine)
调用 将生成器与编译器生成的状态机实例stateMachine
相关联。- 生成器应调用
stateMachine.MoveNext()
在状态机之前Start()
或之后Start()
调用。
- 生成器应调用
- 返回后
Start()
,该方法async
将调用builder.Task
任务以从异步方法返回。 - 每次调用
stateMachine.MoveNext()
都将推进状态机。 - 如果状态机成功完成,
builder.SetResult()
则调用该方法返回值(如果有)。 - 否则,如果在状态机中引发异常
e
,builder.SetException(e)
则调用。 - 如果状态机到达
await expr
表达式,expr.GetAwaiter()
则调用。 - 如果 awaiter 实现
ICriticalNotifyCompletion
且IsCompleted
为 false,状态机将builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)
调用 。AwaitUnsafeOnCompleted()
应在等待程序完成时使用awaiter.UnsafeOnCompleted(action)
该调用进行调用。Action
stateMachine.MoveNext()
- 否则,状态机将
builder.AwaitOnCompleted(ref awaiter, ref stateMachine)
调用 。AwaitOnCompleted()
应在等待程序完成时使用awaiter.OnCompleted(action)
该调用进行调用。Action
stateMachine.MoveNext()
SetStateMachine(IAsyncStateMachine)
编译器生成的IAsyncStateMachine
实现可以调用以标识与状态机实例关联的生成器实例,尤其是对于将状态机实现为值类型的情况。- 如果生成器调用
stateMachine.SetStateMachine(stateMachine)
,将调用stateMachine
builder.SetStateMachine(stateMachine)
生成器实例。
- 如果生成器调用
注意:对于这两者
SetResult(T result)
,«TaskType»<T> Task { get; }
参数和参数必须分别可转换为T
标识。 这样,任务类型生成器就可以支持元组等类型,其中两种不同类型是可转换的标识。 end note
15.15.3 任务返回异步函数的计算
调用任务返回异步函数会导致生成返回的任务类型的实例。 这称为 异步函数的返回任务 。 任务最初处于 不完整 状态。
然后计算异步函数主体,直到挂起(通过到达 await 表达式)或终止,此时控件将返回到调用方以及返回任务。
异步函数的正文终止时,返回任务将移出不完整的状态:
- 如果函数正文作为到达返回语句或正文的末尾而终止,则返回任务中记录任何结果值,该任务将 置于成功 状态。
- 如果函数正文因未捕获
OperationCanceledException
而终止,则会在返回任务中记录异常,该任务将 置于已 取消状态。 - 如果函数正文因任何其他未捕获异常(§13.10.6)而终止,则异常将记录在进入 错误 状态的返回任务中。
15.15.4 计算返回 void 的异步函数
如果异步函数 void
的返回类型为,则计算方式与上述方法不同:由于没有返回任何任务,因此该函数将完成情况和异常传达给当前线程的 同步上下文。 同步上下文的确切定义依赖于实现,但表示当前线程正在运行的“where”。 当对返回异步函数的评估 void
开始、成功完成或导致引发未捕获的异常时,将通知同步上下文。
这样,上下文就可以跟踪其下运行的异步函数数 void
,并决定如何传播传出它们的异常。