C#6.0语言规范(十) 类
类是可以包含数据成员(常量和字段),函数成员(方法,属性,事件,索引器,运算符,实例构造函数,析构函数和静态构造函数)和嵌套类型的数据结构。类类型支持继承,这是一种派生类可以扩展和专门化基类的机制。
类声明
一个class_declaration是type_declaration(类型声明,声明一个新的类)。
1 class_declaration 2 : attributes? class_modifier* 'partial'? 'class' identifier type_parameter_list? 3 class_base? type_parameter_constraints_clause* class_body ';'? 4 ;
class_declaration包括一组可选的属性(属性),随后一组可选的class_modifier S(类改性剂),随后任选的partial
改性剂,接着由关键字class
和标识符名称的类,接着任选的type_parameter_list(输入参数),然后是可选的class_base规范(类基本规范),后跟一组可选的type_parameter_constraints_clause(类型参数约束),后跟一个class_body(类体),可选地后跟分号。
类声明不能提供type_parameter_constraints_clause s,除非它还提供type_parameter_list。
提供type_parameter_list的类声明是泛型类声明。此外,嵌套在泛型类声明或泛型结构声明中的任何类本身都是泛型类声明,因为必须提供包含类型的类型参数才能创建构造类型。
类修饰符
class_declaration可以任选地包括类改性剂的一个序列:
1 class_modifier 2 : 'new' 3 | 'public' 4 | 'protected' 5 | 'internal' 6 | 'private' 7 | 'abstract' 8 | 'sealed' 9 | 'static' 10 | class_modifier_unsafe 11 ;
同一修饰符在类声明中多次出现是编译时错误。
new
嵌套类允许使用修饰符。它指定该类以相同的名称隐藏继承的成员,如“新修饰符”中所述。new
修饰符出现在不是嵌套类声明的类声明上是编译时错误。
在public
,protected
,internal
,和private
修饰符控制类的可访问性。根据发生类声明的上下文,可能不允许某些修饰符(声明的可访问性)。
的abstract
,sealed
和static
改性剂在下面的章节中讨论。
抽象类
所述abstract
改性剂被用来指示一个类是不完整的,并且其旨在仅作为一个基类。抽象类与非抽象类的区别在于以下方面:
- 抽象类不能直接实例化,
new
在抽象类上使用运算符是编译时错误。虽然可以使编译时类型为抽象的变量和值,但这些变量和值必然是null
或包含对从抽象类型派生的非抽象类的实例的引用。 - 允许(但不是必需)抽象类来包含抽象成员。
- 抽象类不能被密封。
当非抽象类派生自抽象类时,非抽象类必须包含所有继承抽象成员的实际实现,从而覆盖那些抽象成员。在这个例子中
1 abstract class A 2 { 3 public abstract void F(); 4 } 5 6 abstract class B: A 7 { 8 public void G() {} 9 } 10 11 class C: B 12 { 13 public override void F() { 14 // actual implementation of F 15 } 16 }
抽象类A
引入了一个抽象方法F
。类B
引入了另一种方法G
,但由于它没有提供实现F
,B
因此还必须声明为abstract。类C
重写F
并提供实际的实现。由于没有抽象成员C
,C
允许(但不要求)是非抽象的。
密封类
该sealed
改性剂是用来防止从派生类。如果将密封类指定为另一个类的基类,则会发生编译时错误。
密封类也不能是抽象类。
该sealed
改性剂主要用于防止意外派生,但它也使某些运行时优化。特别是,因为已知密封类永远不会有任何派生类,所以可以将密封类实例上的虚函数成员调用转换为非虚拟调用。
静态类
该static
改性剂是用来标记被声明为一个类的静态类。静态类无法实例化,不能用作类型,只能包含静态成员。只有静态类才能包含扩展方法的声明(扩展方法)。
静态类声明受以下限制:
- 静态类可能不包含
sealed
或abstract
修饰符。但请注意,由于静态类无法实例化或派生自,因此它的行为就像密封和抽象一样。 - 静态类可能不包含class_base规范(类基本规范),并且不能显式指定基类或已实现接口的列表。静态类隐式继承自类型
object
。 - 静态类只能包含静态成员(静态成员和实例成员)。请注意,常量和嵌套类型被归类为静态成员。
- 静态类不能具有成员
protected
或具有protected internal
可访问性。
违反任何这些限制是编译时错误。
静态类没有实例构造函数。无法在静态类中声明实例构造函数,并且不为静态类提供默认实例构造函数(默认构造函数)。
静态类的成员不是自动静态的,并且成员声明必须显式包含static
修饰符(常量和嵌套类型除外)。当类嵌套在静态外部类中时,嵌套类不是静态类,除非它明确包含static
修饰符。
引用静态类类型
namespace_or_type_name(命名空间和类型名称)被允许,如果引用静态类
- 该namespace_or_type_name是
T
在一个namespace_or_type_name形式的T.I
,或 - 该namespace_or_type_name是
T
在typeof_expression(参数列表的形式的1)typeof(T)
。
primary_expression(功能部件)被允许,如果引用静态类
- 该primary_expression是
E
在member_access(编译时动态过载分辨率检查的形式的)E.I
。
在任何其他上下文中,引用静态类是编译时错误。例如,将静态类用作基类,成员的组成类型(嵌套类型),泛型类型参数或类型参数约束是错误的。同样,静态类不能用于数组类型,指针类型,new
表达式,强制转换表达式,is
表达式,as
表达式,sizeof
表达式或默认值表达式。
部分编辑
所述partial
改性剂被用来表明这class_declaration是部分类型声明。在封闭命名空间或类型声明中具有相同名称的多个部分类型声明组合在一起,形成一个类型声明,遵循在部分类型中指定的规则。
如果在不同的上下文中生成或维护这些段,则将类的声明分布在程序文本的不同段上是有用的。例如,类声明的一部分可以是机器生成的,而另一部分是手动编写的。两者的文本分离可防止一方更新与另一方更新冲突。
输入参数
类型参数是一个简单的标识符,表示为创建构造类型而提供的类型参数的占位符。类型参数是稍后将提供的类型的正式占位符。相反,类型参数(类型参数)是在创建构造类型时替换类型参数的实际类型。
1 type_parameter_list 2 : '<' type_parameters '>' 3 ; 4 5 type_parameters 6 : attributes? type_parameter 7 | type_parameters ',' attributes? type_parameter 8 ; 9 10 type_parameter 11 : identifier 12 ;
类声明中的每个类型参数都在该类的声明空间(声明)中定义一个名称。因此,它不能与另一个类型参数或在该类中声明的成员具有相同的名称。类型参数不能与类型本身具有相同的名称。
类别基础规范
类声明可以包括class_base规范,该规范定义类的直接基类和由类直接实现的接口(接口)。
1 class_base 2 : ':' class_type 3 | ':' interface_type_list 4 | ':' class_type ',' interface_type_list 5 ; 6 7 interface_type_list 8 : interface_type (',' interface_type)* 9 ;
类声明中指定的基类可以是构造的类类型(构造类型)。基类本身不能是类型参数,但它可能涉及范围内的类型参数。
class Extend<V>: V {} // Error, type parameter used as base class
基础类
当class_type包含在class_base中时,它指定要声明的类的直接基类。如果类声明没有class_base,或者class_base仅列出接口类型,则假定直接基类为object
。类继承其直接基类中的成员,如继承中所述。
在这个例子中
1 class A {} 2 3 class B: A {}
类A
被认为是直接基类B
,B
据说是源自A
。由于A
没有明确指定直接基类,因此它的直接基类是隐式的object
。
对于构造的类类型,如果在泛型类声明中指定了基类,则通过为基类声明中的每个type_parameter替换构造类型的相应type_argument来获得构造类型的基类。给出泛型类声明
1 class B<U,V> {...} 2 3 class G<T>: B<string,T[]> {...}
构造类型的基类G<int>
将是B<string,int[]>
。
类类型的直接基类必须至少与类类型本身(可访问性域)一样可访问。例如,public
类是从类private
或internal
类派生的,这是编译时错误。
直接基类的类类型的不能是以下任一类型的:System.Array
,System.Delegate
,System.MulticastDelegate
,System.Enum
,或System.ValueType
。此外,泛型类声明不能System.Attribute
用作直接或间接基类。
而确定直接基类规范的意义A
的一类B
,直接基类的B
暂时假定object
。直观地,这确保了基类规范的含义不能递归地依赖于它自己。这个例子:
1 class A<T> { 2 public class B {} 3 } 4 5 class C : A<C.B> {}
是错误的,因为在基类规范A<C.B>
中直接基类C
被认为是object
,因此(通过命名空间和类型名称的规则) C
不被认为有成员B
。
类类型的基类是直接基类及其基类。换句话说,基类集是直接基类关系的传递闭包。参考上面的例子,B
是A
和的基类object
。在这个例子中
1 class A {...} 2 3 class B<T>: A {...} 4 5 class C<T>: B<IComparable<T>> {...} 6 7 class D<T>: C<T[]> {...}
基类D<int>
是C<int[]>
,B<IComparable<int[]>>
,A
,和object
。
除了类之外object
,每个类类型都只有一个直接基类。本object
类没有直接基类,并且是最终的基类,所有其他类。
当类B
派生自类时A
,A
依赖于编译时错误B
。类直接依赖于它的直接基类(如果有的话),并且直接依赖于它立即嵌套的类(如果有的话)。给定这个定义,类所依赖的完整类集是直接依赖于关系的自反和传递闭包。
这个例子
class A: A {}
是错误的,因为类依赖于自己。同样,这个例子
1 class A: B {} 2 class B: C {} 3 class C: A {}
是错误的,因为类循环依赖于自己。最后,这个例子
1 class A: B.C {} 2 3 class B: A 4 { 5 public class C {} 6 }
导致编译时错误,因为A
依赖于B.C
(它的直接基类),它依赖于B
(它直接封闭的类),它依赖于它A
。
请注意,类不依赖于嵌套在其中的类。在这个例子中
1 class A 2 { 3 class B: A {} 4 }
B
取决于A
(因为它A
是它的直接基类和它的直接封闭类),但A
不依赖于B
(因为B
既不是基类也不是封闭类A
)。因此,该示例是有效的。
不可能从一个sealed
类派生。在这个例子中
1 sealed class A {} 2 3 class B: A {} // Error, cannot derive from a sealed class
class B
是错误的,因为它试图从sealed
类派生A
。
接口实现
class_base规范可以包括接口类型的列表,在这种情况下,类被认为直接实现给定的接口类型。接口实现在接口实现中进一步讨论。
输入参数约束
通用类型和方法声明可以通过包含type_parameter_constraints_clause来选择性地指定类型参数约束。
1 type_parameter_constraints_clause 2 : 'where' type_parameter ':' type_parameter_constraints 3 ; 4 5 type_parameter_constraints 6 : primary_constraint 7 | secondary_constraints 8 | constructor_constraint 9 | primary_constraint ',' secondary_constraints 10 | primary_constraint ',' constructor_constraint 11 | secondary_constraints ',' constructor_constraint 12 | primary_constraint ',' secondary_constraints ',' constructor_constraint 13 ; 14 15 primary_constraint 16 : class_type 17 | 'class' 18 | 'struct' 19 ; 20 21 secondary_constraints 22 : interface_type 23 | type_parameter 24 | secondary_constraints ',' interface_type 25 | secondary_constraints ',' type_parameter 26 ; 27 28 constructor_constraint 29 : 'new' '(' ')' 30 ;
每个type_parameter_constraints_clause都包含令牌where
,后跟类型参数的名称,后跟冒号和该类型参数的约束列表。where
每个类型参数最多只能有一个子句,并且where
子句可以按任何顺序列出。与属性访问器中的标记get
和set
标记一样,where
标记不是关键字。
where
子句中给出的约束列表可以按以下顺序包括以下任何组件:单个主要约束,一个或多个辅助约束以及构造函数约束new()
。
主要约束可以是类类型或引用类型约束 class
或值类型约束 struct
。辅助约束可以是type_parameter或interface_type。
引用类型约束指定用于type参数的类型参数必须是引用类型。已知为引用类型(如下定义)的所有类类型,接口类型,委托类型,数组类型和类型参数都满足此约束。
值类型约束指定用于type参数的类型参数必须是非可空值类型。具有值类型约束的所有非可空结构类型,枚举类型和类型参数都满足此约束。请注意,虽然归类为值类型,但可空类型(Nullable类型)不满足值类型约束。具有值类型约束的类型参数也不能具有constructor_constraint。
永远不允许指针类型为类型参数,并且不认为指针类型满足引用类型或值类型约束。
如果约束是类类型,接口类型或类型参数,则该类型指定用于该类型参数的每个类型参数必须支持的最小“基本类型”。无论何时使用构造类型或泛型方法,都会在编译时根据类型参数的约束检查type参数。提供的类型参数必须满足满足约束条件中描述的条件。
一个class_type约束必须满足下列规则:
- 类型必须是类类型。
- 类型不能是
sealed
。 - 该类型不能是以下类型之一:
System.Array
,System.Delegate
,System.Enum
,或System.ValueType
。 - 类型不能是
object
。因为所有类型派生自object
,所以如果允许这样的约束将不起作用。 - 给定类型参数的最多一个约束可以是类类型。
指定为interface_type约束的类型必须满足以下规则:
- 类型必须是接口类型。
- 在给定子
where
句中不得多次指定类型。
在任何一种情况下,约束都可以将相关类型或方法声明的任何类型参数作为构造类型的一部分,并且可以涉及声明的类型。
指定为类型参数约束的任何类或接口类型必须至少与声明的泛型类型或方法一样可访问(可访问性约束)。
指定为type_parameter约束的类型必须满足以下规则:
- 类型必须是类型参数。
- 在给定子
where
句中不得多次指定类型。
此外,类型参数的依赖关系图中必须没有循环,其中依赖关系是由以下内容定义的传递关系:
- 如果类型参数
T
用作类型参数的约束,S
则S
依赖于T
。 - 如果类型参数
S
取决于类型参数T
并且T
取决于类型参数,U
那么S
取决于U
。
鉴于这种关系,类型参数依赖于自身(直接或间接)是编译时错误。
任何约束必须在依赖类型参数之间保持一致。如果type参数S
依赖于type参数,T
那么:
T
必须没有值类型约束。否则,T
有效密封,因此S
将被强制为相同的类型T
,从而消除了对两种类型参数的需要。- 如果
S
具有值类型约束,则T
不得具有class_type约束。 - 如果
S
有一个class_type约束A
和T
具有class_type约束B
,则必须存在的标识转换或从隐式引用转换A
到B
或从隐式引用转换B
到A
。 - 如果
S
还取决于类型参数U
和U
具有class_type约束A
和T
具有class_type约束B
,则必须存在的标识转换或从隐式引用转换A
到B
或从隐式引用转换B
到A
。
S
具有值类型约束并T
具有引用类型约束是有效的。有效这限制T
的类型System.Object
,System.ValueType
,System.Enum
,和任何接口类型。
如果where
类型参数的子句包含构造函数约束(具有该形式new()
),则可以使用new
运算符创建该类型的实例(对象创建表达式)。用于具有构造函数约束的类型参数的任何类型参数必须具有公共无参数构造函数(此构造函数对任何值类型隐式存在)或者是具有值类型约束或构造函数约束的类型参数(有关详细信息,请参阅类型参数约束)。
以下是约束的示例:
1 interface IPrintable 2 { 3 void Print(); 4 } 5 6 interface IComparable<T> 7 { 8 int CompareTo(T value); 9 } 10 11 interface IKeyProvider<T> 12 { 13 T GetKey(); 14 } 15 16 class Printer<T> where T: IPrintable {...} 17 18 class SortedList<T> where T: IComparable<T> {...} 19 20 class Dictionary<K,V> 21 where K: IComparable<K> 22 where V: IPrintable, IKeyProvider<K>, new() 23 { 24 ... 25 }
以下示例出错,因为它会在类型参数的依赖关系图中导致循环:
1 class Circular<S,T> 2 where S: T 3 where T: S // Error, circularity in dependency graph 4 { 5 ... 6 }
以下示例说明了其他无效情况:
1 class Sealed<S,T> 2 where S: T 3 where T: struct // Error, T is sealed 4 { 5 ... 6 } 7 8 class A {...} 9 10 class B {...} 11 12 class Incompat<S,T> 13 where S: A, T 14 where T: B // Error, incompatible class-type constraints 15 { 16 ... 17 } 18 19 class StructWithClass<S,T,U> 20 where S: struct, T 21 where T: U 22 where U: A // Error, A incompatible with struct 23 { 24 ... 25 }
类型参数的有效基类T
定义如下:
- 如果
T
没有主要约束或类型参数约束,则其有效基类为object
。 - 如果
T
具有值类型约束,则其有效基类为System.ValueType
。 - 如果
T
有class_type约束C
但没有type_parameter约束,则其有效基类为C
。 - 如果
T
没有class_type约束但具有一个或多个type_parameter约束,则其有效基类是其type_parameter约束的有效基类集中包含程度最大的类型(提升转换运算符)。一致性规则确保存在这种最包含的类型。 - 如果
T
同时具有class_type约束和一个或多个type_parameter约束,则其有效基类是最涵盖类型(提升转换运算符在由的集合)class_type的约束T
和其有效基类type_parameter约束。一致性规则确保存在这种最包含的类型。 - 如果
T
具有引用类型约束但没有class_type约束,则其有效基类为object
。
出于这些规则的目的,如果T具有value_type的约束V
,则使用最具体的基类型,即class_type。这在一个显式给定的约束中永远不会发生,但是当泛型方法的约束由重写方法声明或接口方法的显式实现隐式继承时,可能会发生这种情况。V
这些规则确保有效基类始终是class_type。
类型参数的有效接口集T
定义如下:
- 如果
T
没有secondary_constraints,则其有效接口集为空。 - 如果
T
具有interface_type约束但没有type_parameter约束,则其有效接口集是其interface_type约束集。 - 如果
T
没有interface_type约束但具有type_parameter约束,则其有效接口集是其type_parameter约束的有效接口集的并集。 - 如果同时
T
具有interface_type约束和type_parameter约束,则其有效接口集是其interface_type约束集和其type_parameter约束的有效接口集的并集。
如果类型参数具有引用类型约束或其有效基类不是或者,则已知类型参数是引用类型。object
System.ValueType
约束类型参数类型的值可用于访问约束隐含的实例成员。在这个例子中
1 interface IPrintable 2 { 3 void Print(); 4 } 5 6 class Printer<T> where T: IPrintable 7 { 8 void PrintOne(T x) { 9 x.Print(); 10 } 11 }
IPrintable
可以直接调用的方法x
因为T
被约束为始终实现IPrintable
。
类体
类的class_body定义该类的成员。
1 class_body 2 : '{' class_member_declaration* '}' 3 ;
部分类型
类型声明可以跨多个部分类型声明进行拆分。类型声明是按照本节中的规则从其各部分构造的,因此在程序的编译时和运行时处理的剩余时间内将其视为单个声明。
如果class_declaration,struct_declaration或interface_declaration包含partial
修饰符,则它表示部分类型声明。partial
不是关键字,只有当它出现在其中一个关键字之前class
,struct
或者interface
在类型声明中,或者在void
方法声明中的类型之前时,它才会充当修饰符。在其他情况下,它可以用作普通标识符。
部分类型声明的每个部分都必须包含一个partial
修饰符。它必须具有相同的名称,并在与其他部分相同的命名空间或类型声明中声明。所述partial
改性剂表明类型声明的附加部分可以存在于其他地方,但这种额外的部件的存在不是必需的; 它对于具有单个声明的类型有效,以包含partial
修饰符。
必须将部分类型的所有部分编译在一起,以便可以在编译时将这些部分合并为单个类型声明。部分类型特别不允许扩展已编译的类型。
可以使用partial
修饰符在多个部分中声明嵌套类型。通常,使用类型声明包含类型partial
,并且嵌套类型的每个部分都在包含类型的不同部分中声明。
partial
委托或枚举声明中不允许使用修饰符。
属性
通过以未指定的顺序组合每个部分的属性来确定部分类型的属性。如果属性放在多个部分上,则相当于在类型上多次指定属性。例如,这两部分:
1 [Attr1, Attr2("hello")] 2 partial class A {} 3 4 [Attr3, Attr2("goodbye")] 5 partial class A {}
等同于以下声明:
1 [Attr1, Attr2("hello"), Attr3, Attr2("goodbye")] 2 class A {}
类型参数的属性以类似的方式组合。
修饰符
当分部类型声明包括:可访问性规范(的public
,protected
,internal
,和private
改性剂),它必须与包括辅助功能规范的所有其他部分一致。如果部分类型的任何部分都不包含可访问性规范,则为该类型提供适当的默认可访问性(声明的可访问性)。
如果嵌套类型的一个或多个部分声明包含new
修饰符,则如果嵌套类型隐藏继承成员(隐藏继承),则不会报告警告。
如果类的一个或多个部分声明包含abstract
修饰符,则该类被视为抽象类(抽象类)。否则,该类被认为是非抽象的。
如果类的一个或多个部分声明包含sealed
修饰符,则该类被视为已密封(密封类)。否则,该课程被视为未开封。
请注意,类不能是抽象的和密封的。
当unsafe
修饰符用于部分类型声明时,只将该特定部分视为不安全的上下文(Unsafe contexts)。
输入参数和约束
如果泛型类型在多个部分中声明,则每个部分必须声明类型参数。每个部分必须具有相同数量的类型参数,并且每个类型参数的名称依次相同。
当部分泛型类型声明包含约束(where
子句)时,约束必须与包含约束的所有其他部分一致。具体而言,包含约束的每个部分必须对同一组类型参数具有约束,并且对于每个类型参数,主要,次要和构造函数约束的集合必须是等效的。如果两组约束包含相同的成员,则它们是等效的。如果部分泛型类型的任何部分都没有指定类型参数约束,则类型参数被视为不受约束。
这个例子
1 partial class Dictionary<K,V> 2 where K: IComparable<K> 3 where V: IKeyProvider<K>, IPersistable 4 { 5 ... 6 } 7 8 partial class Dictionary<K,V> 9 where V: IPersistable, IKeyProvider<K> 10 where K: IComparable<K> 11 { 12 ... 13 } 14 15 partial class Dictionary<K,V> 16 { 17 ... 18 }
是正确的,因为那些包含约束的部分(前两个)分别有效地为同一组类型参数指定了相同的主要,次要和构造函数约束集。
基类
当部分类声明包含基类规范时,它必须与包含基类规范的所有其他部分一致。如果部分类的任何部分都不包含基类规范,则基类变为System.Object
(基类)。
基础接口
在多个部分中声明的类型的基接口集是每个部件上指定的基接口的并集。特定的基本接口只能在每个部件上命名一次,但允许多个部件命名相同的基本接口。必须只有一个任何给定基接口成员的实现。
在这个例子中
1 partial class C: IA, IB {...} 2 3 partial class C: IC {...} 4 5 partial class C: IA, IB {...}
类的一组基本接口C
是IA
,IB
,和IC
。
通常,每个部分都提供在该部分上声明的接口的实现; 但是,这不是必需的。部件可以为在不同部件上声明的接口提供实现:
1 partial class X 2 { 3 int IComparable.CompareTo(object o) {...} 4 } 5 6 partial class X: IComparable 7 { 8 ... 9 }
成员
除了部分方法(部分方法)之外,在多个部分中声明的类型的成员集只是每个部分中声明的成员集的并集。类型声明的所有部分的主体共享相同的声明空间(声明),并且每个成员(范围)的范围扩展到所有部分的主体。任何成员的可访问域始终包括封闭类型的所有部分; private
在一个部分中声明的成员可以从另一个部分自由访问。在类型的多个部分中声明相同成员是编译时错误,除非该成员是具有partial
修饰符的类型。
1 partial class A 2 { 3 int x; // Error, cannot declare x more than once 4 5 partial class Inner // Ok, Inner is a partial type 6 { 7 int y; 8 } 9 } 10 11 partial class A 12 { 13 int x; // Error, cannot declare x more than once 14 15 partial class Inner // Ok, Inner is a partial type 16 { 17 int z; 18 } 19 }
类型中成员的排序对于C#代码来说很少有意义,但在与其他语言和环境交互时可能很重要。在这些情况下,未定义在多个部分中声明的类型内的成员的顺序。
部分方法
部分方法可以在类型声明的一部分中定义,也可以在另一部分中实现。实施是可选的; 如果没有部分实现部分方法,则部分方法声明和对它的所有调用都将从部件组合产生的类型声明中删除。
部分方法不能定义访问修饰符,但是是隐式的private
。它们的返回类型必须是void
,并且它们的参数不能包含out
修饰符。仅当标识partial
出现在void
类型之前时,标识符才会在方法声明中被识别为特殊关键字; 否则它可以用作普通标识符。部分方法无法显式实现接口方法。
有两种部分方法声明:如果方法声明的主体是分号,则声明称为定义的部分方法声明。如果将主体作为块给出,则声明被称为实现部分方法声明。在类型声明的各个部分中,只能有一个定义具有给定签名的部分方法声明,并且只能有一个使用给定签名实现部分方法声明。如果给出了实现的部分方法声明,则必须存在相应的定义部分方法声明,并且声明必须与以下内容中指定的匹配:
- 声明必须具有相同的修饰符(尽管不一定是相同的顺序),方法名称,类型参数的数量和参数的数量。
- 声明中的相应参数必须具有相同的修饰符(尽管不一定是相同的顺序)和相同的类型(类型参数名称中的模数差异)。
- 声明中的相应类型参数必须具有相同的约束(类型参数名称中的模数差异)。
实现部分方法声明可以出现在与相应的定义部分方法声明相同的部分中。
只有定义的部分方法才能参与重载决策。因此,无论是否给出实现声明,调用表达式都可以解析为部分方法的调用。因为部分方法总是返回void
,所以这样的调用表达式将始终是表达式语句。此外,由于部分方法是隐式的private
,因此这些语句将始终出现在声明部分方法的类型声明的一个部分中。
如果部分类型声明的任何部分都不包含给定部分方法的实现声明,则只需从组合类型声明中删除调用它的任何表达式语句。因此,调用表达式(包括任何组成表达式)在运行时无效。部分方法本身也被删除,不会是组合类型声明的成员。
如果给定的部分方法存在实现声明,则保留部分方法的调用。除了以下内容之外,partial方法产生类似于实现部分方法声明的方法声明:
- 在
partial
不包括修改 - 结果方法声明中的属性是未定义顺序的定义和实现部分方法声明的组合属性。不删除重复项。
- 结果方法声明的参数属性是未定义顺序的定义和实现部分方法声明的相应参数的组合属性。不删除重复项。
如果为部分方法M提供了定义声明但未给出实现声明,则以下限制适用:
- 创建方法委托(委托创建表达式)是编译时错误。
M
在匿名函数内部引用转换为表达式树类型(评估匿名函数转换为表达式树类型)是编译时错误。- 作为调用的一部分而出现的表达式
M
不会影响明确的赋值状态(Definite assignment),这可能会导致编译时错误。 M
不能成为应用程序的入口点(应用程序启动)。
部分方法对于允许类型声明的一部分定制另一部分的行为(例如,由工具生成的部分)是有用的。考虑以下部分类声明:
1 partial class Customer 2 { 3 string name; 4 5 public string Name { 6 get { return name; } 7 set { 8 OnNameChanging(value); 9 name = value; 10 OnNameChanged(); 11 } 12 13 } 14 15 partial void OnNameChanging(string newName); 16 17 partial void OnNameChanged(); 18 }
如果在没有任何其他部分的情况下编译此类,则将删除定义的部分方法声明及其调用,并且生成的组合类声明将等效于以下内容:
1 class Customer 2 { 3 string name; 4 5 public string Name { 6 get { return name; } 7 set { name = value; } 8 } 9 }
但是,假设给出了另一部分,它提供了部分方法的实现声明:
1 partial class Customer 2 { 3 partial void OnNameChanging(string newName) 4 { 5 Console.WriteLine("Changing " + name + " to " + newName); 6 } 7 8 partial void OnNameChanged() 9 { 10 Console.WriteLine("Changed to " + name); 11 } 12 }
然后生成的组合类声明将等效于以下内容:
1 class Customer 2 { 3 string name; 4 5 public string Name { 6 get { return name; } 7 set { 8 OnNameChanging(value); 9 name = value; 10 OnNameChanged(); 11 } 12 13 } 14 15 void OnNameChanging(string newName) 16 { 17 Console.WriteLine("Changing " + name + " to " + newName); 18 } 19 20 void OnNameChanged() 21 { 22 Console.WriteLine("Changed to " + name); 23 } 24 }
名称绑定
尽管可扩展类型的每个部分都必须在同一名称空间中声明,但这些部分通常是在不同的名称空间声明中编写的。因此,每个部分可能存在不同的using
指令(使用指令)。在一个部分中解释简单名称(类型推断)时,只using
考虑包含该部分的命名空间声明的指令。这可能导致相同的标识符在不同的部分具有不同的含义:
1 namespace N 2 { 3 using List = System.Collections.ArrayList; 4 5 partial class A 6 { 7 List x; // x has type System.Collections.ArrayList 8 } 9 } 10 11 namespace N 12 { 13 using List = Widgets.LinkedList; 14 15 partial class A 16 { 17 List y; // y has type Widgets.LinkedList 18 } 19 }
类成员
类的成员由其class_member_declaration引入的成员和从直接基类继承的成员组成。
1 class_member_declaration 2 : constant_declaration 3 | field_declaration 4 | method_declaration 5 | property_declaration 6 | event_declaration 7 | indexer_declaration 8 | operator_declaration 9 | constructor_declaration 10 | destructor_declaration 11 | static_constructor_declaration 12 | type_declaration 13 ;
类类型的成员分为以下类别:
- 常量,表示与类关联的常量值(常量)。
- 字段,它们是类的变量(字段)。
- 方法,实现可由类(方法)执行的计算和操作。
- 属性,定义命名特征以及与读取和写入这些特征相关的操作(属性)。
- 事件,定义可由类生成的通知(事件)。
- 索引器,允许以与数组(索引器)相同的方式(语法)索引类的实例。
- 运算符,用于定义可应用于类(运算符)实例的表达式运算符。
- 实例构造函数,它实现初始化类实例所需的操作(实例构造函数)
- 执行在类的实例之前执行的操作的析构函数被永久丢弃(析构函数)。
- 静态构造函数,它实现初始化类本身所需的操作(静态构造函数)。
- 类型,表示类的本地类型(嵌套类型)。
可以包含可执行代码的成员统称为类类型的函数成员。类类型的函数成员是该类类型的方法,属性,事件,索引器,运算符,实例构造函数,析构函数和静态构造函数。
一个class_declaration创建一个新的声明空间(声明),以及class_member_declaration立即被包含小号class_declaration新成员引入此声明空间。以下规则适用于class_member_declaration:
- 实例构造函数,析构函数和静态构造函数必须与紧邻的类具有相同的名称。所有其他成员的名称必须与紧邻的类的名称不同。
- 常量,字段,属性,事件或类型的名称必须与同一类中声明的所有其他成员的名称不同。
- 方法的名称必须与同一类中声明的所有其他非方法的名称不同。此外,方法的签名(签名和重载)必须不同于在同一个类中声明的所有其他方法的签名,并且在同一个类中声明的两个方法可能没有仅由
ref
和区别的签名out
。 - 实例构造函数的签名必须不同于在同一个类中声明的所有其他实例构造函数的签名,并且在同一个类中声明的两个构造函数可能没有仅由
ref
和区别的签名out
。 - 索引器的签名必须与同一类中声明的所有其他索引器的签名不同。
- 运算符的签名必须与同一类中声明的所有其他运算符的签名不同。
类类型的继承成员(继承)不是类的声明空间的一部分。因此,允许派生类声明与继承成员具有相同名称或签名的成员(实际上隐藏了继承的成员)。
实例类型
每个类声明都有一个关联的绑定类型(绑定和未绑定类型),即实例类型。对于泛型类声明,实例类型是通过从类型声明创建构造类型(构造类型)形成的,每个提供的类型参数都是相应的类型参数。由于实例类型使用类型参数,因此只能在类型参数在范围内使用; 也就是说,在类声明中。实例类型是this
在类声明中编写的代码的类型。对于非泛型类,实例类型只是声明的类。以下显示了几个类声明及其实例类型:
1 class A<T> // instance type: A<T> 2 { 3 class B {} // instance type: A<T>.B 4 class C<U> {} // instance type: A<T>.C<U> 5 } 6 7 class D {} // instance type: D
构造类型的成员
通过对成员声明中的每个type_parameter替换构造类型的相应type_argument来获得构造类型的非继承成员。替换过程基于类型声明的语义含义,而不仅仅是文本替换。
例如,给定泛型类声明
1 class Gen<T,U> 2 { 3 public T[,] a; 4 public void G(int i, T t, Gen<U,T> gt) {...} 5 public U Prop { get {...} set {...} } 6 public int H(double d) {...} 7 }
构造的类型Gen<int[],IComparable<string>>
具有以下成员:
1 public int[,][] a; 2 public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...} 3 public IComparable<string> Prop { get {...} set {...} } 4 public int H(double d) {...}
a
泛型类声明中成员的类型Gen
是“二维数组T
”,因此a
上面构造类型中成员的类型是“二维数组的一维数组int
”,或int[,][]
。
在实例函数成员中,类型this
是包含声明的实例类型(实例类型)。
泛型类的所有成员都可以直接或作为构造类型的一部分使用来自任何封闭类的类型参数。当在运行时使用特定的闭合构造类型(打开和关闭类型)时,类型参数的每次使用都将替换为提供给构造类型的实际类型参数。例如:
1 class C<V> 2 { 3 public V f1; 4 public C<V> f2 = null; 5 6 public C(V x) { 7 this.f1 = x; 8 this.f2 = this; 9 } 10 } 11 12 class Application 13 { 14 static void Main() { 15 C<int> x1 = new C<int>(1); 16 Console.WriteLine(x1.f1); // Prints 1 17 18 C<double> x2 = new C<double>(3.1415); 19 Console.WriteLine(x2.f1); // Prints 3.1415 20 } 21 }
继承
类继承其直接基类类型的成员。继承意味着类隐式包含其直接基类类型的所有成员,但基类的实例构造函数,析构函数和静态构造函数除外。继承的一些重要方面是:
- 继承是传递性的。如果
C
派生自B
和B
派生自A
,则C
继承声明的成员B
以及声明的成员A
。 - 派生类扩展其直接基类。派生类可以向其继承的成员添加新成员,但不能删除继承成员的定义。
- 实例构造函数,析构函数和静态构造函数不是继承的,但所有其他成员都是,无论其声明的可访问性(成员访问)如何。但是,根据其声明的可访问性,可能无法在派生类中访问继承的成员。
- 派生类可以通过声明具有相同名称或签名的新成员来隐藏(隐藏继承)继承成员。但请注意,隐藏继承的成员不会删除该成员 - 它只是使该成员直接通过派生类无法访问。
- 类的实例包含在类及其基类中声明的所有实例字段的集合,并且从派生类类型到其任何基类类型存在隐式转换(隐式引用转换)。因此,对某个派生类的实例的引用可以被视为对其任何基类的实例的引用。
- 类可以声明虚方法,属性和索引器,派生类可以覆盖这些函数成员的实现。这使类能够展示多态行为,其中由函数成员调用执行的操作根据调用该函数成员的实例的运行时类型而变化。
构造类类型的继承成员是直接基类类型(基类)的成员,通过在class_base规范中为每个出现的相应类型参数替换构造类型的类型参数来找到它。这些成员,反过来,由代,对于每一个变换type_parameter在成员声明,相应type_argument所述的class_base规范。
1 class B<U> 2 { 3 public U F(long index) {...} 4 } 5 6 class D<T>: B<T[]> 7 { 8 public T G(string s) {...} 9 }
在上面的示例中,构造的类型D<int>
具有public int G(string s)
通过将类型参数替换为类型参数int
而获得的非继承成员T
。D<int>
还有一个来自类声明的继承成员B
。这个继承的成员是通过首先通过在基类规范中替换来确定基类类型B<int[]>
来确定的。然后,作为一种类型的参数,代替在,产生继承的成员。D<int>
int
T
B<T[]>
B
int[]
U
public U F(long index)
public int[] F(long index)
新修饰符
class_member_declaration允许声明一个部件用相同的名称或签名一个继承的成员。发生这种情况时,称派生类成员隐藏基类成员。隐藏继承的成员不被视为错误,但它确实会导致编译器发出警告。要禁止警告,派生类成员的声明可以包含一个new
修饰符,以指示派生成员旨在隐藏基本成员。在隐藏继承中进一步讨论了该主题。
如果new
修饰符包含在不隐藏继承成员的声明中,则会发出对该效果的警告。通过删除new
修改器可以抑制此警告。
访问修饰符
一个class_member_declaration可以有五种可能的种声明可访问性(中的任何一个声明可访问): ,public
,protected internal
,protected
,internal
或private
。除protected internal
组合外,指定多个访问修饰符是编译时错误。当class_member_declaration不包含任何访问修饰符时,private
假设。
构成类型
成员声明中使用的类型称为该成员的组成类型。可能的组成类型是常量,字段,属性,事件或索引器的类型,方法或运算符的返回类型,以及方法,索引器,运算符或实例构造函数的参数类型。成员的组成类型必须至少与该成员本身一样可访问(可访问性约束)。
静态和实例成员
类的成员是静态成员或实例成员。一般来说,将静态成员视为属于类类型,将实例成员视为属于对象(类类型的实例)是有用的。
当field,method,property,event,operator或constructor声明包含一个static
修饰符时,它声明一个静态成员。另外,常量或类型声明隐式声明了静态成员。静态成员具有以下特征:
- 在表单
M
的member_access(成员访问)中引用静态成员时E.M
,E
必须表示包含的类型M
。E
表示实例是编译时错误。 - 静态字段确切地标识要由给定封闭类类型的所有实例共享的一个存储位置。无论创建给定封闭类类型的多少实例,都只有静态字段的一个副本。
- 静态函数成员(方法,属性,事件,运算符或构造函数)不对特定实例进行操作,并且
this
在此类函数成员中引用它是编译时错误。
当字段,方法,属性,事件,索引器,构造函数或析构函数声明不包含static
修饰符时,它声明实例成员。(实例成员有时称为非静态成员。)实例成员具有以下特征:
- 在表单
M
的member_access(成员访问权限)中引用实例成员时E.M
,E
必须表示包含类型的实例M
。E
表示类型是绑定时错误。 - 类的每个实例都包含该类的所有实例字段的单独集合。
- 实例函数成员(方法,属性,索引器,实例构造函数或析构函数)在类的给定实例上运行,并且此实例可以作为
this
(此访问)进行访问。
以下示例说明了访问静态和实例成员的规则:
1 class Test 2 { 3 int x; 4 static int y; 5 6 void F() { 7 x = 1; // Ok, same as this.x = 1 8 y = 1; // Ok, same as Test.y = 1 9 } 10 11 static void G() { 12 x = 1; // Error, cannot access this.x 13 y = 1; // Ok, same as Test.y = 1 14 } 15 16 static void Main() { 17 Test t = new Test(); 18 t.x = 1; // Ok 19 t.y = 1; // Error, cannot access static member through instance 20 Test.x = 1; // Error, cannot access instance member through type 21 Test.y = 1; // Ok 22 } 23 }
该F
方法显示在实例函数成员中,simple_name(简单名称)可用于访问实例成员和静态成员。该G
方法显示在静态函数成员中,通过simple_name访问实例成员是编译时错误。该Main
方法显示在member_access(成员访问)中,必须通过实例访问实例成员,并且必须通过类型访问静态成员。
嵌套类型
在类或结构声明中声明的类型称为嵌套类型。在编译单元或命名空间中声明的类型称为非嵌套类型。
在这个例子中
1 using System; 2 3 class A 4 { 5 class B 6 { 7 static void F() { 8 Console.WriteLine("A.B.F"); 9 } 10 } 11 }
class B
是嵌套类型,因为它在类中声明A
,而class A
是非嵌套类型,因为它是在编译单元中声明的。
完全限定名称
完全限定域名(完全限定名)嵌套的类型是S.N
哪里S
是哪种类型的类型的完全合格的名称N
声明。
声明可访问性
非嵌套类型可以具有public
或internal
声明可访问性,并且internal
默认情况下已声明可访问性。嵌套类型也可以具有这些形式的已声明可访问性,以及一个或多个其他形式的已声明可访问性,具体取决于包含类型是类还是结构:
- 即在一个类中声明嵌套类型可以具有任意的五种形式的声明可访问(的
public
,protected internal
,protected
,internal
,或private
),并且,象其它类的成员,默认为private
声明可访问性。 - 其是在结构中声明嵌套类型可以具有任意的三种形式声明可访问(的
public
,internal
或private
),并像其他结构成员,默认为private
声明可访问性。
这个例子
1 public class List 2 { 3 // Private data structure 4 private class Node 5 { 6 public object Data; 7 public Node Next; 8 9 public Node(object data, Node next) { 10 this.Data = data; 11 this.Next = next; 12 } 13 } 14 15 private Node first = null; 16 private Node last = null; 17 18 // Public interface 19 public void AddToFront(object o) {...} 20 public void AddToBack(object o) {...} 21 public object RemoveFromFront() {...} 22 public object RemoveFromBack() {...} 23 public int Count { get {...} } 24 }
声明一个私有嵌套类Node
。
名称隐藏
嵌套类型可以隐藏(名称隐藏)基础成员。new
嵌套类型声明允许使用修饰符,以便可以显式表达隐藏。这个例子
1 using System; 2 3 class Base 4 { 5 public static void M() { 6 Console.WriteLine("Base.M"); 7 } 8 } 9 10 class Derived: Base 11 { 12 new public class M 13 { 14 public static void F() { 15 Console.WriteLine("Derived.M.F"); 16 } 17 } 18 } 19 20 class Test 21 { 22 static void Main() { 23 Derived.M.F(); 24 } 25 }
显示一个M
隐藏在其中M
定义的方法的嵌套类Base
。
这种访问
嵌套类型及其包含类型与this_access(此访问权限)没有特殊关系。具体来说,this
嵌套类型中的内容不能用于引用包含类型的实例成员。如果嵌套类型需要访问其包含类型的实例成员,则可以通过为this
包含类型的实例提供嵌套类型的构造函数参数来提供访问。以下示例
1 using System; 2 3 class C 4 { 5 int i = 123; 6 7 public void F() { 8 Nested n = new Nested(this); 9 n.G(); 10 } 11 12 public class Nested 13 { 14 C this_c; 15 16 public Nested(C c) { 17 this_c = c; 18 } 19 20 public void G() { 21 Console.WriteLine(this_c.i); 22 } 23 } 24 } 25 26 class Test 27 { 28 static void Main() { 29 C c = new C(); 30 c.F(); 31 } 32 }
显示了这种技术。实例C
创建的实例,Nested
并将其自身的this
到Nested
的构造,以提供后续访问C
的实例成员。
访问包含类型的私有和受保护成员
嵌套类型可以访问其包含类型可访问的所有成员,包括具有private
和protected
声明可访问性的包含类型的成员。这个例子
1 using System; 2 3 class C 4 { 5 private static void F() { 6 Console.WriteLine("C.F"); 7 } 8 9 public class Nested 10 { 11 public static void G() { 12 F(); 13 } 14 } 15 } 16 17 class Test 18 { 19 static void Main() { 20 C.Nested.G(); 21 } 22 }
显示C
包含嵌套类的类Nested
。在此内部Nested
,该方法G
调用其中F
定义的静态方法C
,并F
具有私有声明的可访问性。
嵌套类型还可以访问在其包含类型的基类型中定义的受保护成员。在这个例子中
1 using System; 2 3 class Base 4 { 5 protected void F() { 6 Console.WriteLine("Base.F"); 7 } 8 } 9 10 class Derived: Base 11 { 12 public class Nested 13 { 14 public void G() { 15 Derived d = new Derived(); 16 d.F(); // ok 17 } 18 } 19 } 20 21 class Test 22 { 23 static void Main() { 24 Derived.Nested n = new Derived.Nested(); 25 n.G(); 26 } 27 }
嵌套类通过调用实例来Derived.Nested
访问在基类中F
定义的受保护方法。Derived
Base
Derived
泛型类中的嵌套类型
泛型类声明可以包含嵌套类型声明。封闭类的类型参数可以在嵌套类型中使用。嵌套类型声明可以包含仅适用于嵌套类型的其他类型参数。
泛型类声明中包含的每个类型声明都是隐式的泛型类型声明。当编写对嵌套在泛型类型中的类型的引用时,必须命名包含的构造类型,包括其类型参数。但是,从外部类中,可以无限制地使用嵌套类型; 在构造嵌套类型时,可以隐式使用外部类的实例类型。以下示例显示了三种不同的正确方法来引用从中创建的构造类型Inner
; 前两个是等价的:
1 class Outer<T> 2 { 3 class Inner<U> 4 { 5 public static void F(T t, U u) {...} 6 } 7 8 static void F(T t) { 9 Outer<T>.Inner<string>.F(t, "abc"); // These two statements have 10 Inner<string>.F(t, "abc"); // the same effect 11 12 Outer<int>.Inner<string>.F(3, "abc"); // This type is different 13 14 Outer.Inner<string>.F(t, "abc"); // Error, Outer needs type arg 15 } 16 }
虽然编程风格很糟糕,但嵌套类型中的类型参数可以隐藏在外部类型中声明的成员或类型参数:
1 class Outer<T> 2 { 3 class Inner<T> // Valid, hides Outer's T 4 { 5 public T t; // Refers to Inner's T 6 } 7 }
保留的成员名称
为了促进底层C#运行时实现,对于作为属性,事件或索引器的每个源成员声明,实现必须根据成员声明的类型,名称和类型保留两个方法签名。如果程序声明其签名与这些保留签名之一匹配的成员,即使基础运行时实现未使用这些保留,也是编译时错误。
保留名称不会引入声明,因此它们不参与成员查找。但是,声明的关联保留方法签名确实参与继承(继承),并且可以使用new修饰符(新修饰符)隐藏。
保留这些名称有三个目的:
允许底层实现使用普通标识符作为获取或设置对C#语言功能的访问权限的方法名称。
允许其他语言使用普通标识符作为方法名称进行互操作,以获取或设置对C#语言功能的访问权限。
通过使保留的成员名称的细节在所有C#实现中保持一致,帮助确保一个符合标准的编译器接受的源被另一个接受。
析构函数(Destructors)的声明也会导致保留签名(为析构函数保留的成员名称)。
为属性保留的成员名称
对于类型的属性P(Properties),T保留以下签名:
1 T get_P(); 2 void set_P(T value);
即使属性是只读或只写,两个签名都是保留的。
在这个例子中
1 using System; 2 3 class A 4 { 5 public int P { 6 get { return 123; } 7 } 8 } 9 10 class B: A 11 { 12 new public int get_P() { 13 return 456; 14 } 15 16 new public void set_P(int value) { 17 } 18 } 19 20 class Test 21 { 22 static void Main() { 23 B b = new B(); 24 A a = b; 25 Console.WriteLine(a.P); 26 Console.WriteLine(b.P); 27 Console.WriteLine(b.get_P()); 28 } 29 }
类A
定义了只读属性P
,因此保留了签名get_P
和set_P
方法。类B
派生自A
并隐藏这两个保留的签名。该示例生成输出:
1 123 2 123 3 456
为活动保留签名
对于委托类型的事件E
(事件),T
保留以下签名:
1 void add_E(T handler); 2 void remove_E(T handler);
为索引器保留签名
对于带参数列表的类型的索引器(索引器),保留以下签名:T
L
1 T get_Item(L); 2 void set_Item(L, T value);
即使索引器是只读的或只写的,两个签名都是保留的。
此外,会员名称Item
是保留的。
为析构函数保留的成员名称
对于包含析构函数(Destructors)的类,保留以下签名:
void Finalize();
常量
常数是一个类的成员,它表示的恒定值:可在编译时计算的值。constant_declaration引入了一个给定类型的一个或多个常数。
1 constant_declaration 2 : attributes? constant_modifier* 'const' type constant_declarators ';' 3 ; 4 5 constant_modifier 6 : 'new' 7 | 'public' 8 | 'protected' 9 | 'internal' 10 | 'private' 11 ; 12 13 constant_declarators 14 : constant_declarator (',' constant_declarator)* 15 ; 16 17 constant_declarator 18 : identifier '=' constant_expression 19 ;
constant_declaration可以包括一组的属性(属性),一个new改性剂(new修饰符),和四个访问修饰符(的有效组合访问改性剂)。属性和修饰符适用于constant_declaration声明的所有成员。尽管常量被认为是静态成员,但constant_declaration既不需要也不允许static修饰符。同一修饰符在常量声明中多次出现是错误的。
该类型一的constant_declaration指定由该声明引入的成员的类型。该类型后面是一个constant_declarator列表,每个都引入一个新成员。constant_declarator由一个的标识符名称的部件,接着是“ =”标记,接着是constant_expression(常量表达式,使该部件的值)。
的类型以恒定的声明中指定必须是sbyte,byte,short,ushort,int,uint,long,ulong,char,float,double,decimal,bool,string,的enum_type或reference_type。每个constant_expression必须生成目标类型或可通过隐式转换(隐式转换)转换为目标类型的类型的值。
常量的类型必须至少与常量本身一样可访问(可访问性约束)。
使用simple_name(简单名称)或member_access(成员访问)在表达式中获取常量的值。
常量本身可以参与constant_expression。因此,常量可以用在需要constant_expression的任何构造中。此类构造的示例包括case标签,goto case语句,enum成员声明,属性和其他常量声明。
如Constant表达式中所述,constant_expression是一个可以在编译时完全计算的表达式。由于创建除reference_type之外的非null值的唯一方法string是应用new运算符,并且由于new在constant_expression中不允许运算符,因此reference_type s的常量唯一可能的值string是null。
当需要常量值的符号名称时,但在常量声明中不允许该值的类型时,或者在编译时无法通过constant_expression计算该值时,可以使用readonly字段(只读字段)代替。
声明多个常量的常量声明等效于具有相同属性,修饰符和类型的单个常量的多个声明。例如
1 class A 2 { 3 public const double X = 1.0, Y = 2.0, Z = 3.0; 4 }
相当于
1 class A 2 { 3 public const double X = 1.0; 4 public const double Y = 2.0; 5 public const double Z = 3.0; 6 }
只要依赖关系不是循环性的,就允许常量依赖于同一程序中的其他常量。编译器自动安排以适当的顺序评估常量声明。在这个例子中
1 class A 2 { 3 public const int X = B.Z + 1; 4 public const int Y = 10; 5 } 6 7 class B 8 { 9 public const int Z = A.Y + 1; 10 }
1 class A 2 { 3 public const int X = B.Z + 1; 4 public const int Y = 10; 5 } 6 7 class B 8 { 9 public const int Z = A.Y + 1; 10 }
编译器首先计算A.Y
,然后计算B.Z
,最后计算A.X
,产生的值10
,11
和12
。常量声明可能依赖于来自其他程序的常量,但这种依赖性只能在一个方向上进行。参考上面的例子,如果A
并且B
在单独的程序中声明,则可能A.X
依赖于B.Z
,但B.Z
可能不会同时依赖A.Y
。
字段
字段是表示与对象或类关联的变量的成员。field_declaration引入了一个给定类型的一个或多个字段。
1 field_declaration 2 : attributes? field_modifier* type variable_declarators ';' 3 ; 4 5 field_modifier 6 : 'new' 7 | 'public' 8 | 'protected' 9 | 'internal' 10 | 'private' 11 | 'static' 12 | 'readonly' 13 | 'volatile' 14 | field_modifier_unsafe 15 ; 16 17 variable_declarators 18 : variable_declarator (',' variable_declarator)* 19 ; 20 21 variable_declarator 22 : identifier ('=' variable_initializer)? 23 ; 24 25 variable_initializer 26 : expression 27 | array_initializer 28 ;
一个field_declaration可以包括一组属性(属性),一个new
修饰符(new修饰符),四个访问修饰符(的有效组合访问修饰符)和static
改性剂(静态和实例字段)。另外,field_declaration可以包括readonly
修饰符(Readonly fields)或volatile
修饰符(Volatile fields),但不包括两者。属性和修饰符适用于field_declaration声明的所有成员。同一修饰符在字段声明中多次出现是错误的。
该类型一的field_declaration指定由该声明引入的成员的类型。该类型后面是一个variable_declarator列表,每个列表都引入了一个新成员。variable_declarator由一个的标识符名称该成员,任选地随后通过“ =
”令牌和variable_initializer(变量初始值设定),以显示该构件的初始值。
字段的类型必须至少与字段本身一样可访问(可访问性约束)。
使用simple_name(简单名称)或member_access(成员访问)在表达式中获取字段的值。使用赋值(赋值运算符)修改非只读字段的值。使用后缀递增和递减运算符(Postfix递增和递减运算符)和前缀递增和递减运算符(前缀递增和递减运算符)可以获得和修改非只读字段的值。
声明多个字段的字段声明等效于具有相同属性,修饰符和类型的单个字段的多个声明。例如
1 class A 2 { 3 public static int X = 1, Y, Z = 100; 4 }
相当于
1 class A 2 { 3 public static int X = 1; 4 public static int Y; 5 public static int Z = 100; 6 }
静态和实例字段
当字段声明包含static
修饰符时,声明引入的字段是静态字段。如果不存在static
修饰符,则声明引入的字段是实例字段。静态字段和实例字段是C#支持的几种变量(变量)中的两种,有时它们分别称为静态变量和实例变量。
静态字段不是特定实例的一部分; 相反,它在封闭类型的所有实例(开放和封闭类型)之间共享。无论创建了多少封闭类类型的实例,关联的应用程序域都只有一个静态字段的副本。
例如:
1 class C<V> 2 { 3 static int count = 0; 4 5 public C() { 6 count++; 7 } 8 9 public static int Count { 10 get { return count; } 11 } 12 } 13 14 class Application 15 { 16 static void Main() { 17 C<int> x1 = new C<int>(); 18 Console.WriteLine(C<int>.Count); // Prints 1 19 20 C<double> x2 = new C<double>(); 21 Console.WriteLine(C<int>.Count); // Prints 1 22 23 C<int> x3 = new C<int>(); 24 Console.WriteLine(C<int>.Count); // Prints 2 25 } 26 }
实例字段属于实例。具体来说,类的每个实例都包含该类的所有实例字段的单独集合。
当在表单的member_access(成员访问)中引用字段时E.M
,如果M
是静态字段,则E
必须表示包含的类型M
,如果M
是实例字段,则E必须表示包含类型的实例M
。
静态和实例成员之间的差异中进一步讨论了静态和实例成员。
只读字段
当field_declaration包含readonly
修饰符时,声明引入的字段是只读字段。对只读字段的直接赋值只能作为该声明的一部分或在同一类中的实例构造函数或静态构造函数中出现。(在这些上下文中可以多次分配只读字段。)具体而言,readonly
仅在以下上下文中允许直接分配字段:
- 在variable_declarator引入该字段(通过包括variable_initializer在声明)。
- 对于实例字段,在包含字段声明的类的实例构造函数中; 对于静态字段,在包含字段声明的类的静态构造函数中。这些也是唯一的上下文中,它是有效的传递一个
readonly
场作为一个out
或ref
参数。
试图分配给readonly
字段或将其传递作为out
或ref
在任何其他上下文参数是一个编译时间错误。
使用常量的静态只读字段
static readonly
当期望为恒定值的符号名称字段是有用的,但是当该值的类型在一个不允许const
声明时,或者当不能在编译时计算的值。在这个例子中
1 public class Color 2 { 3 public static readonly Color Black = new Color(0, 0, 0); 4 public static readonly Color White = new Color(255, 255, 255); 5 public static readonly Color Red = new Color(255, 0, 0); 6 public static readonly Color Green = new Color(0, 255, 0); 7 public static readonly Color Blue = new Color(0, 0, 255); 8 9 private byte red, green, blue; 10 11 public Color(byte r, byte g, byte b) { 12 red = r; 13 green = g; 14 blue = b; 15 } 16 }
Black
,White
,Red
,Green
,和Blue
成员不能被声明为const
成员,因为它们的值不能在编译时计算。但是,声明它们static readonly
会产生相同的效果。
常量和静态只读字段的版本控制
常量和只读字段具有不同的二进制版本语义。当表达式引用常量时,常量的值在编译时获得,但是当表达式引用只读字段时,直到运行时才获得该字段的值。考虑一个由两个独立程序组成的应用程序:
1 using System; 2 3 namespace Program1 4 { 5 public class Utils 6 { 7 public static readonly int X = 1; 8 } 9 } 10 11 namespace Program2 12 { 13 class Test 14 { 15 static void Main() { 16 Console.WriteLine(Program1.Utils.X); 17 } 18 } 19 }
在Program1
和Program2
命名空间表示两个程序,分别编译。因为Program1.Utils.X
声明为静态只读字段,所以Console.WriteLine
语句输出的值在编译时是未知的,而是在运行时获得的。因此,如果X
更改了值并Program1
重新编译,则Console.WriteLine
即使Program2
未重新编译,语句也将输出新值。但是,如果X
一直是常量,那么X
在Program2
编译的时候就会获得的值,并且在重新编译Program1
之前不会受到更改的影响Program2
。
易失性字段
当field_declaration包含volatile
修饰符时,该声明引入的字段是易失性字段。
对于非易失性字段,重新排序指令的优化技术可能会在多线程程序中导致意外和不可预测的结果,这些程序无需同步即可访问字段,例如lock_statement(锁定语句)提供的字段。这些优化可以由编译器,运行时系统或硬件执行。对于易失性字段,此类重新排序优化受到限制:
- 读取volatile字段称为volatile读取。易失性读取具有“获取语义”; 也就是说,保证在指令序列之后的任何内存引用之前发生。
- 写入易失性字段称为易失性写入。易失性写入具有“释放语义”; 也就是说,保证在指令序列中的写指令之前的任何存储器引用之后发生。
这些限制确保所有线程将按照执行顺序观察由任何其他线程执行的易失性写入。从所有执行线程看,不需要符合要求的实现来提供易失写入的单个总排序。volatile字段的类型必须是以下之一:
- 一个reference_type。
- 类型
byte
,sbyte
,short
,ushort
,int
,uint
,char
,float
,bool
,System.IntPtr
,或System.UIntPtr
。 - 一个enum_type具有枚举基类型
byte
,sbyte
,short
,ushort
,int
,或uint
。
这个例子
1 using System; 2 using System.Threading; 3 4 class Test 5 { 6 public static int result; 7 public static volatile bool finished; 8 9 static void Thread2() { 10 result = 143; 11 finished = true; 12 } 13 14 static void Main() { 15 finished = false; 16 17 // Run Thread2() in a new thread 18 new Thread(new ThreadStart(Thread2)).Start(); 19 20 // Wait for Thread2 to signal that it has a result by setting 21 // finished to true. 22 for (;;) { 23 if (finished) { 24 Console.WriteLine("result = {0}", result); 25 return; 26 } 27 } 28 } 29 }
产生输出:
result = 143
在此示例中,该方法Main
启动一个运行该方法的新线程Thread2
。此方法将值存储到名为的非易失性字段中result
,然后存储true
在volatile字段中finished
。主线程等待字段finished
设置为true
,然后读取字段result
。自finished
声明以来volatile
,主线程必须143
从字段中读取值result
。如果字段finished
尚未声明volatile
,则允许存储result
在存储之后对主线程可见finished
,因此主线程可以0
从字段中读取值result
。声明finished
为volatile
字段可防止出现任何此类不一致。
字段初始化
字段的初始值(无论是静态字段还是实例字段)是字段类型的默认值(默认值)。在此默认初始化发生之前,无法观察字段的值,因此字段永远不会“未初始化”。这个例子
1 using System; 2 3 class Test 4 { 5 static bool b; 6 int i; 7 8 static void Main() { 9 Test t = new Test(); 10 Console.WriteLine("b = {0}, i = {1}", b, t.i); 11 } 12 }
产生输出
b = False, i = 0
因为b
并且i
都自动初始化为默认值。
变量初始化器
字段声明可能包含variable_initializer。对于静态字段,变量初始值设定项对应于在类初始化期间执行的赋值语句。对于实例字段,变量初始值设定项对应于在创建类的实例时执行的赋值语句。
这个例子
1 using System; 2 3 class Test 4 { 5 static double x = Math.Sqrt(2.0); 6 int i = 100; 7 string s = "Hello"; 8 9 static void Main() { 10 Test a = new Test(); 11 Console.WriteLine("x = {0}, i = {1}, s = {2}", x, a.i, a.s); 12 } 13 }
产生输出
x = 1.4142135623731, i = 100, s = Hello
因为x
当静态字段初始值设定项执行和赋值时i
,s
会发生赋值,并在实例字段初始值设定项执行时发生。
字段初始化中描述的默认值初始化适用于所有字段,包括具有可变初始值设定项的字段。因此,当初始化类时,首先将该类中的所有静态字段初始化为其默认值,然后以文本顺序执行静态字段初始值设定项。同样,当创建类的实例时,首先将该实例中的所有实例字段初始化为其默认值,然后以文本顺序执行实例字段初始化程序。
具有可变初始值设定项的静态字段可以在其默认值状态下被观察到。然而,作为一种风格问题,强烈建议不要这样做。这个例子
1 using System; 2 3 class Test 4 { 5 static int a = b + 1; 6 static int b = a + 1; 7 8 static void Main() { 9 Console.WriteLine("a = {0}, b = {1}", a, b); 10 } 11 }
表现出这种行为。尽管a和b的循环定义,该程序是有效的。它导致输出
a = 1, b = 2
因为静态字段a
和b
初始化为0
(默认值int
),在他们的初始化被执行。当用于a
运行的初始化程序时,值b
为零,因此a
初始化为1
。当用于b
运行的初始化程序时,值a
已经是1
,因此b
被初始化为2
。
静态字段初始化
类的静态字段变量初始值设定项对应于以它们出现在类声明中的文本顺序执行的赋值序列。如果类中存在静态构造函数(静态构造函数),则在执行该静态构造函数之前立即执行静态字段初始值设定项。否则,静态字段初始化器在第一次使用该类的静态字段之前的实现相关时间执行。这个例子
1 using System; 2 3 class Test 4 { 5 static void Main() { 6 Console.WriteLine("{0} {1}", B.Y, A.X); 7 } 8 9 public static int F(string s) { 10 Console.WriteLine(s); 11 return 1; 12 } 13 } 14 15 class A 16 { 17 public static int X = Test.F("Init A"); 18 } 19 20 class B 21 { 22 public static int Y = Test.F("Init B"); 23 }
可能产生输出:
1 Init A 2 Init B 3 1 1
或输出:
1 Init B 2 Init A 3 1 1
因为X
初始化程序和初始化程序的执行Y
可以按任何顺序执行; 它们仅限于在引用这些字段之前发生。但是,在示例中:
1 using System; 2 3 class Test 4 { 5 static void Main() { 6 Console.WriteLine("{0} {1}", B.Y, A.X); 7 } 8 9 public static int F(string s) { 10 Console.WriteLine(s); 11 return 1; 12 } 13 } 14 15 class A 16 { 17 static A() {} 18 19 public static int X = Test.F("Init A"); 20 } 21 22 class B 23 { 24 static B() {} 25 26 public static int Y = Test.F("Init B"); 27 }
输出必须是:
1 Init B 2 Init A 3 1 1
因为静态构造函数执行时的规则(如静态构造函数中所定义)规定B
静态构造函数(因此B
的静态字段初始值设定项)必须在A
静态构造函数和字段初始值设定项之前运行。
实例字段初始化
类的实例字段变量初始值设定项对应于在进入该类的任何一个实例构造函数(构造函数初始值设定项)时立即执行的赋值序列。变量初始值设定项以它们出现在类声明中的文本顺序执行。实例构建器中进一步描述了类实例创建和初始化过程。
实例字段的变量初始值设定项无法引用正在创建的实例。因此,this
在变量初始化程序中引用是编译时错误,因为变量初始化程序通过simple_name引用任何实例成员是编译时错误。在这个例子中
1 class A 2 { 3 int x = 1; 4 int y = x + 1; // Error, reference to instance member of this 5 }
变量初始值设定项y
导致编译时错误,因为它引用了正在创建的实例的成员。
方法
方法是实现可以由对象或类执行的计算或操作的部件。方法使用method_declaration声明:
1 method_declaration 2 : method_header method_body 3 ; 4 5 method_header 6 : attributes? method_modifier* 'partial'? return_type member_name type_parameter_list? 7 '(' formal_parameter_list? ')' type_parameter_constraints_clause* 8 ; 9 10 method_modifier 11 : 'new' 12 | 'public' 13 | 'protected' 14 | 'internal' 15 | 'private' 16 | 'static' 17 | 'virtual' 18 | 'sealed' 19 | 'override' 20 | 'abstract' 21 | 'extern' 22 | 'async' 23 | method_modifier_unsafe 24 ; 25 26 return_type 27 : type 28 | 'void' 29 ; 30 31 member_name 32 : identifier 33 | interface_type '.' identifier 34 ; 35 36 method_body 37 : block 38 | '=>' expression ';' 39 | ';' 40 ;
一个method_declaration可以包括一组属性(属性)和四个访问修饰符的有效组合(访问修饰符),在new
(new修饰符), static
(静态和实例方法), virtual
(虚方法), override
(覆盖方法), sealed
(密封方法),abstract
(抽象方法)和extern
(外部方法)修饰符。
如果满足以下所有条件,则声明具有有效的修饰符组合:
- 声明包括访问修饰符(访问修饰符)的有效组合。
- 声明多次不包含相同的修饰符。
- 该声明包含下列修饰符最多一个:
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
。
具有async
修饰符的方法是异步函数,并遵循异步函数中描述的规则。
方法声明的return_type指定方法计算和返回的值的类型。该return_type是void
如果方法不返回值。如果声明包含partial
修饰符,则返回类型必须为void
。
该MEMBER_NAME指定方法的名称。除非该方法是显式接口成员实现(显式接口成员实现),否则member_name只是一个标识符。对于显式接口成员实现,member_name由interface_type后跟“ .
”和标识符组成。
可选的type_parameter_list指定方法的类型参数(类型参数)。如果指定了type_parameter_list,则该方法是通用方法。如果方法具有extern
修饰符,则无法指定type_parameter_list。
可选的formal_parameter_list指定方法的参数(Method parameters)。
可选的type_parameter_constraints_clause指定对各个类型参数的约束(类型参数约束),并且只有在还提供了type_parameter_list时才可以指定,并且该方法没有override
修饰符。
的return_type并且每个被引用类型的formal_parameter_list的方法的必须至少与方法本身(如可访问辅助约束)。
所述method_body或者是分号,一个语句体或表达体。语句体由一个块组成,该块指定在调用方法时要执行的语句。表达式主体由=>
后跟表达式和分号组成,并表示在调用方法时要执行的单个表达式。
对于abstract
和extern
方法,method_body只包含一个分号。对于partial
方法,method_body可以由分号,块体或表达体组成。对于所有其他方法,method_body是块体或表达式体。
如果method_body由分号组成,则声明可能不包含async
修饰符。
方法的名称,类型参数列表和形式参数列表定义方法的签名(签名和重载)。具体而言,方法的签名包括其名称,类型参数的数量以及其形式参数的数量,修饰符和类型。出于这些目的,在形式参数类型中出现的方法的任何类型参数不是通过其名称来标识,而是通过其在方法的类型参数列表中的序号位置来标识。返回类型不是方法签名的一部分,也不是类型参数或形式参数的名称。
方法的名称必须与同一类中声明的所有其他非方法的名称不同。此外,方法的签名必须与同一类中声明的所有其他方法的签名不同,并且在同一个类中声明的两个方法可能不具有仅由ref
和区别的签名out
。
该方法的type_parameter s为在整个范围method_declaration,并且可以用来形成贯穿类型在该范围return_type,method_body和type_parameter_constraints_clause秒,但不是在属性。
所有形式参数和类型参数必须具有不同的名称。
方法参数
方法的参数(如果有)由方法的formal_parameter_list声明。
1 formal_parameter_list 2 : fixed_parameters 3 | fixed_parameters ',' parameter_array 4 | parameter_array 5 ; 6 7 fixed_parameters 8 : fixed_parameter (',' fixed_parameter)* 9 ; 10 11 fixed_parameter 12 : attributes? parameter_modifier? type identifier default_argument? 13 ; 14 15 default_argument 16 : '=' expression 17 ; 18 19 parameter_modifier 20 : 'ref' 21 | 'out' 22 | 'this' 23 ; 24 25 parameter_array 26 : attributes? 'params' array_type identifier 27 ;
形式参数列表由一个或多个逗号分隔的参数组成,其中只有最后一个参数可以是parameter_array。
fixed_parameter包括一组可选的属性(属性),任选的ref,out或this改性剂,式中,标识符和可选的default_argument。每个fixed_parameter都使用给定名称声明给定类型的参数。所述this改性剂指定方法作为扩展方法和只允许在一个静态方法的第一个参数。扩展方法进一步描述于扩展方法。
具有default_argument的fixed_parameter称为可选参数,而不带default_argument的fixed_parameter则是必需参数。在formal_parameter_list中的可选参数之后,可能不会显示必需参数。
一个ref或out参数不能有default_argument。的表达在default_argument必须是以下之一:
一个constant_expression
表单的表达式,new S()其中S是值类型
表单的表达式,default(S)其中S是值类型
的表达式必须是通过一个标识或可空转化为参数的类型隐式转换。
如果可选参数出现在实现部分方法声明(部分方法),显式接口成员实现(显式接口成员实现)或单参数索引器声明(索引器)中,编译器应该发出警告,因为这些成员永远不会以允许省略参数的方式调用。
parameter_array包括一组可选的属性(属性),一个params调节剂,ARRAY_TYPE和标识符。参数数组声明具有给定名称的给定数组类型的单个参数。参数数组的array_type必须是一维数组类型(数组类型)。在方法调用中,参数数组允许指定给定数组类型的单个参数,或者允许指定数组元素类型的零个或多个参数。参数数组在参数数组中进一步描述。
一个parameter_array一个可选的参数后可能会发生,但不能有默认值-的论点一遗漏parameter_array反而会导致创建一个空数组。
以下示例说明了不同类型的参数:
1 public void M( 2 ref int i, 3 decimal d, 4 bool b = false, 5 bool? n = false, 6 string s = "Hello", 7 object o = null, 8 T t = default(T), 9 params int[] a 10 ) { }
在formal_parameter_list为M
,i
是一个需要ref参数,d
是一个所需的值参数,b
,s
,o
和t
是可选的值和参数a
是参数数组。
方法声明为参数,类型参数和局部变量创建单独的声明空间。名称通过类型参数列表和方法的形式参数列表以及方法块中的局部变量声明引入此声明空间。方法声明空间的两个成员具有相同的名称是错误的。方法声明空间和嵌套声明空间的局部变量声明空间包含具有相同名称的元素是错误的。
方法调用(Method invocations)创建特定于该调用的方法的形式参数和局部变量的副本,并且调用的参数列表将值或变量引用分配给新创建的形式参数。在方法块中,形式参数可以通过simple_name表达式(简单名称)中的标识符引用。
有四种形式参数:
- 值参数,声明时不带任何修饰符。
- 引用参数,使用
ref
修饰符声明。 - 输出参数,使用
out
修饰符声明。 - 参数数组,使用
params
修饰符声明。
如签名和重载中所述,ref
和out
修饰符是方法签名的一部分,但params
修饰符不是。
值参数
使用无修饰符声明的参数是值参数。值参数对应于局部变量,该局部变量从方法调用中提供的相应参数获取其初始值。
当形式参数是值参数时,方法调用中的相应参数必须是可隐式转换(隐式转换)到形式参数类型的表达式。
允许方法将新值分配给值参数。此类分配仅影响值参数表示的本地存储位置 - 它们对方法调用中给出的实际参数没有影响。
引用参数
使用ref
修饰符声明的参数是引用参数。与值参数不同,引用参数不会创建新的存储位置。相反,引用参数表示与作为方法调用中的参数给出的变量相同的存储位置。
当形式参数是引用参数时,方法调用中的相应参数必须由关键字ref
后跟一个与形式参数相同类型的variable_reference(确定明确赋值的精确规则)组成。必须明确赋值变量才能将其作为引用参数传递。
在方法中,始终认为引用参数是明确分配的。
声明为迭代器(迭代器)的方法不能具有引用参数。
这个例子
1 using System; 2 3 class Test 4 { 5 static void Swap(ref int x, ref int y) { 6 int temp = x; 7 x = y; 8 y = temp; 9 } 10 11 static void Main() { 12 int i = 1, j = 2; 13 Swap(ref i, ref j); 14 Console.WriteLine("i = {0}, j = {1}", i, j); 15 } 16 }
产生输出
i = 2, j = 1
用于调用Swap
in Main
,x
表示i
和y
表示j
。因此,调用具有交换的值的作用i
和j
。
在采用参考参数的方法中,多个名称可以表示相同的存储位置。在这个例子中
1 class A 2 { 3 string s; 4 5 void F(ref string a, ref string b) { 6 s = "One"; 7 a = "Two"; 8 b = "Three"; 9 } 10 11 void G() { 12 F(ref s, ref s); 13 } 14 }
的调用F
中G
传递到参考s
两个a
和b
。因此,对于该调用,名称s
,a
和b
所有引用相同的存储位置,并且三个赋值都修改实例字段s
。
输出参数
使用out
修饰符声明的参数是输出参数。与引用参数类似,输出参数不会创建新的存储位置。相反,输出参数表示与作为方法调用中的参数给出的变量相同的存储位置。
当形式参数是输出参数时,方法调用中的相应参数必须由关键字out
后跟一个与形式参数相同类型的variable_reference(用于确定明确赋值的精确规则)组成。在将变量作为输出参数传递之前,无需明确赋值变量,但在将变量作为输出参数传递的调用之后,该变量被视为明确赋值。
在方法中,就像局部变量一样,输出参数最初被认为是未分配的,必须在使用其值之前明确赋值。
在方法返回之前,必须明确赋值方法的每个输出参数。
声明为部分方法(部分方法)或迭代器(迭代器)的方法不能具有输出参数。
输出参数通常用于产生多个返回值的方法中。例如:
1 using System; 2 3 class Test 4 { 5 static void SplitPath(string path, out string dir, out string name) { 6 int i = path.Length; 7 while (i > 0) { 8 char ch = path[i - 1]; 9 if (ch == '\\' || ch == '/' || ch == ':') break; 10 i--; 11 } 12 dir = path.Substring(0, i); 13 name = path.Substring(i); 14 } 15 16 static void Main() { 17 string dir, name; 18 SplitPath("c:\\Windows\\System\\hello.txt", out dir, out name); 19 Console.WriteLine(dir); 20 Console.WriteLine(name); 21 } 22 }
该示例生成输出:
1 c:\Windows\System\ 2 hello.txt
请注意,dir
和name
变量在传递之前可以取消分配SplitPath
,并且在调用之后它们被认为是明确分配的。
参数数组
使用params
修饰符声明的参数是参数数组。如果形式参数列表包含参数数组,则它必须是列表中的最后一个参数,并且必须是一维数组类型。例如,该类型的string[]
和string[][]
可被用作参数数组的类型,但类型string[,]
不能。这是不可能的结合params
修饰符修饰符ref
和out
。
参数数组允许在方法调用中以两种方式之一指定参数:
- 为参数数组提供的参数可以是可以隐式转换(隐式转换)到参数数组类型的单个表达式。在这种情况下,参数数组的作用与值参数完全相同。
- 或者,调用可以为参数数组指定零个或多个参数,其中每个参数是一个可隐式转换(隐式转换)到参数数组的元素类型的表达式。在这种情况下,调用创建参数数组类型的实例,其长度对应于参数的数量,使用给定的参数值初始化数组实例的元素,并使用新创建的数组实例作为实际参数。
除了在调用中允许可变数量的参数之外,参数数组恰好等效于相同类型的值参数(值参数)。
这个例子
1 using System; 2 3 class Test 4 { 5 static void F(params int[] args) { 6 Console.Write("Array contains {0} elements:", args.Length); 7 foreach (int i in args) 8 Console.Write(" {0}", i); 9 Console.WriteLine(); 10 } 11 12 static void Main() { 13 int[] arr = {1, 2, 3}; 14 F(arr); 15 F(10, 20, 30, 40); 16 F(); 17 } 18 }
产生输出
1 Array contains 3 elements: 1 2 3 2 Array contains 4 elements: 10 20 30 40 3 Array contains 0 elements:
第一次调用F
只是将数组a
作为值参数传递。第二次调用F
自动创建int[]
具有给定元素值的四元素,并将该数组实例作为值参数传递。同样,第三次调用F
创建一个零元素int[]
并将该实例作为值参数传递。第二次和第三次调用完全等同于写:
1 F(new int[] {10, 20, 30, 40}); 2 F(new int[] {});
当执行重载解析时,具有参数数组的方法可以以其正常形式或以其扩展形式(适用的功能成员)应用。只有在方法的正常形式不适用且仅与扩展形式具有相同签名的适用方法尚未在同一类型中声明时,方法的扩展形式才可用。
这个例子
1 using System; 2 3 class Test 4 { 5 static void F(params object[] a) { 6 Console.WriteLine("F(object[])"); 7 } 8 9 static void F() { 10 Console.WriteLine("F()"); 11 } 12 13 static void F(object a0, object a1) { 14 Console.WriteLine("F(object,object)"); 15 } 16 17 static void Main() { 18 F(); 19 F(1); 20 F(1, 2); 21 F(1, 2, 3); 22 F(1, 2, 3, 4); 23 } 24 }
产生输出
1 F(); 2 F(object[]); 3 F(object,object); 4 F(object[]); 5 F(object[]);
在该示例中,具有参数数组的方法的两种可能的扩展形式已经作为常规方法包含在类中。因此,在执行重载解析时不考虑这些扩展形式,因此第一和第三方法调用选择常规方法。当一个类声明一个带有参数数组的方法时,将一些扩展形式作为常规方法包含在内也并不罕见。通过这样做,可以避免在调用具有参数数组的方法的扩展形式时发生的数组实例的分配。
当参数数组的类型是object[]
,在方法的正常形式和单个object
参数的消耗形式之间出现潜在的模糊性。模棱两可的原因object[]
是a本身可以隐式转换为类型object
。然而,模糊性没有问题,因为可以通过在需要时插入强制转换来解决它。
这个例子
1 using System; 2 3 class Test 4 { 5 static void F(params object[] args) { 6 foreach (object o in args) { 7 Console.Write(o.GetType().FullName); 8 Console.Write(" "); 9 } 10 Console.WriteLine(); 11 } 12 13 static void Main() { 14 object[] a = {1, "Hello", 123.456}; 15 object o = a; 16 F(a); 17 F((object)a); 18 F(o); 19 F((object[])o); 20 } 21 }
产生输出
1 System.Int32 System.String System.Double 2 System.Object[] 3 System.Object[] 4 System.Int32 System.String System.Double
在第一次和最后一次调用中F
,正常形式F
是适用的,因为从参数类型到参数类型(两者都是类型object[]
)存在隐式转换。因此,重载决策选择正常形式F
,并将参数作为常规值参数传递。在第二次和第三次调用中,正常形式F
不适用,因为从参数类型到参数类型object
不存在隐式转换(类型不能隐式转换为类型object[]
)。但是,扩展形式F
是适用的,因此通过重载决策来选择。结果,一个元素object[]
由调用创建,并使用给定的参数值(它本身是对a的引用object[]
)初始化数组的单个元素。
静态和实例方法
当方法声明包含static
修饰符时,该方法被称为静态方法。当不存在static
修饰符时,该方法被称为实例方法。
静态方法不在特定实例上运行,并且this
在静态方法中引用它是编译时错误。
实例方法对类的给定实例进行操作,该实例可以作为this
(此访问)进行访问。
当在表单的member_access(成员访问)中引用方法时E.M
,如果M
是静态方法,则E
必须表示包含的类型M
,如果M
是实例方法,则E
必须表示包含类型的实例M
。
静态和实例成员之间的差异中进一步讨论了静态和实例成员。
虚方法
当实例方法声明包含virtual
修饰符时,该方法被称为虚方法。当不存在virtual
修饰符时,该方法被称为非虚方法。
非虚方法的实现是不变的:无论是在声明它的类的实例还是派生类的实例上调用该方法,实现都是相同的。相反,虚拟方法的实现可以被派生类取代。取代继承虚拟方法的实现的过程称为重写该方法(覆盖方法)。
在虚方法调用中,进行该调用的实例的运行时类型决定了要调用的实际方法实现。在非虚方法调用中,实例的编译时类型是决定因素。准确地说,当在具有编译时类型和运行时类型(其中是或者派生自的类)的实例上使用N
参数列表调用named方法时,将按如下方式处理调用:A
C
R
R
C
C
- 首先,过载分辨率就被应用于
C
,N
和A
,选择一个特定的方法M
从该组中声明和继承的方法C
。这在方法调用中描述。 - 然后,如果
M
是非虚方法,M
则调用。 - 否则,
M
是一个虚拟的方法,而最派生实现的M
相对于R
被调用。
对于在类中声明或继承的每个虚方法,存在关于该类的方法的最派生实现。M
关于类的虚拟方法的最派生实现R
确定如下:
- 如果
R
包含引入virtual
声明M
,那么这是最衍生的实现M
。 - 否则,如果
R
包含override
的M
,那么这是最派生实现M
。 - 否则,最派生实现
M
相对于R
相同派生程度最大的实现M
相对于直接基类的R
。
以下示例说明了虚拟和非虚拟方法之间的差异:
1 using System; 2 3 class A 4 { 5 public void F() { Console.WriteLine("A.F"); } 6 7 public virtual void G() { Console.WriteLine("A.G"); } 8 } 9 10 class B: A 11 { 12 new public void F() { Console.WriteLine("B.F"); } 13 14 public override void G() { Console.WriteLine("B.G"); } 15 } 16 17 class Test 18 { 19 static void Main() { 20 B b = new B(); 21 A a = b; 22 a.F(); 23 b.F(); 24 a.G(); 25 b.G(); 26 } 27 }
在该示例中,A
引入了非虚方法F
和虚方法G
。该类B
引入了一个新的非虚方法F
,从而隐藏了继承的方法F
,并且还覆盖了继承的方法G
。该示例生成输出:
1 A.F 2 B.F 3 B.G 4 B.G
请注意,该语句不会a.G()
调用。这是因为实例的运行时类型(即),而不是实例的编译时类型(即),它决定了要调用的实际方法实现。B.G
A.G
B
A
因为允许方法隐藏继承的方法,所以类可以包含具有相同签名的多个虚方法。这不会出现歧义问题,因为隐藏了除派生最多的方法之外的所有方法。在这个例子中
1 using System; 2 3 class A 4 { 5 public virtual void F() { Console.WriteLine("A.F"); } 6 } 7 8 class B: A 9 { 10 public override void F() { Console.WriteLine("B.F"); } 11 } 12 13 class C: B 14 { 15 new public virtual void F() { Console.WriteLine("C.F"); } 16 } 17 18 class D: C 19 { 20 public override void F() { Console.WriteLine("D.F"); } 21 } 22 23 class Test 24 { 25 static void Main() { 26 D d = new D(); 27 A a = d; 28 B b = d; 29 C c = d; 30 a.F(); 31 b.F(); 32 c.F(); 33 d.F(); 34 } 35 }
C
和D
类包含两个虚拟方法具有相同的签名:该一个通过引入A
和一个通过引入C
。引入C
的方法隐藏了继承自的方法A
。因此,覆盖声明D
会覆盖由引入的方法C
,并且不可能D
覆盖由引入的方法A
。该示例生成输出:
1 B.F 2 B.F 3 D.F 4 D.F
请注意,可以通过访问D
未隐藏方法的较少派生类型的实例来调用隐藏虚拟方法。
覆盖方法
当实例方法声明包含override
修饰符时,该方法称为覆盖方法。覆盖方法使用相同的签名覆盖继承的虚方法。虚拟方法声明引入了新方法,而覆盖方法声明通过提供该方法的新实现来专门化现有的继承虚拟方法。
通过覆盖的方法override
声明称为重写的基方法。对于M
在类中声明的重写方法C
,重写的基本方法是通过检查每个基类类型来确定的C
,从直接基类类型开始C
并继续每个连续的直接基类类型,直到至少在给定的基类类型中找到一个可访问的方法,其具有与M
替换类型参数后相同的签名。用于定位所述重写的基方法的目的,方法被认为是可访问的,如果它是public
,如果是protected
,如果是protected internal
,或者如果它是internal
与在相同的程序作为声明C
。
发生编译时错误,除非对覆盖声明满足以下所有条件:
- 可以如上所述定位被重写的基本方法。
- 只有一个这样的重写基本方法。仅当基类类型是构造类型时,此限制才有效,其中类型参数的替换使得两个方法的签名相同。
- 重写的基本方法是虚方法,抽象方法或覆盖方法。换句话说,重写的基本方法不能是静态的或非虚拟的。
- 重写的基本方法不是密封方法。
- 覆盖方法和重写的基本方法具有相同的返回类型。
- 覆盖声明和重写的基本方法具有相同的声明可访问性。换句话说,覆盖声明不能更改虚拟方法的可访问性。但是,如果重写的基本方法在内部受到保护,并且它在与包含override方法的程序集不同的程序集中声明,则必须保护override方法声明的可访问性。
- 覆盖声明不指定type-parameter-constraints-clauses。相反,约束是从重写的基本方法继承的。请注意,重写方法中作为类型参数的约束可能会被继承约束中的类型参数替换。这可能导致在明确指定时不合法的约束,例如值类型或密封类型。
以下示例演示了覆盖规则如何适用于泛型类:
1 abstract class C<T> 2 { 3 public virtual T F() {...} 4 public virtual C<T> G() {...} 5 public virtual void H(C<T> x) {...} 6 } 7 8 class D: C<string> 9 { 10 public override string F() {...} // Ok 11 public override C<string> G() {...} // Ok 12 public override void H(C<T> x) {...} // Error, should be C<string> 13 } 14 15 class E<T,U>: C<U> 16 { 17 public override U F() {...} // Ok 18 public override C<U> G() {...} // Ok 19 public override void H(C<T> x) {...} // Error, should be C<U> 20 }
覆盖声明可以使用base_access(Base访问)访问重写的基本方法。在这个例子中
1 class A 2 { 3 int x; 4 5 public virtual void PrintFields() { 6 Console.WriteLine("x = {0}", x); 7 } 8 } 9 10 class B: A 11 { 12 int y; 13 14 public override void PrintFields() { 15 base.PrintFields(); 16 Console.WriteLine("y = {0}", y); 17 } 18 }
base.PrintFields()
在调用B
调用PrintFields
中声明的方法A
。base_access禁用虚拟调用机制和简单地将基体的方法的非虚拟方法。如果B
已经写入了调用((A)this).PrintFields()
,它将递归调用PrintFields
声明的方法B
,而不是声明的方法A
,因为它PrintFields
是虚拟的,运行时类型((A)this)
是B
。
只有通过包含override
修饰符,方法才能覆盖另一个方法。在所有其他情况下,与继承方法具有相同签名的方法只隐藏继承的方法。在这个例子中
1 class A 2 { 3 public virtual void F() {} 4 } 5 6 class B: A 7 { 8 public virtual void F() {} // Warning, hiding inherited F() 9 }
F
在方法B
不包括override
改性剂,因此不会覆盖F
在方法A
。相反,该F
方法B
隐藏了该方法A
,并且报告了警告,因为声明不包含new
修饰符。
在这个例子中
1 class A 2 { 3 public virtual void F() {} 4 } 5 6 class B: A 7 { 8 new private void F() {} // Hides A.F within body of B 9 } 10 11 class C: B 12 { 13 public override void F() {} // Ok, overrides A.F 14 }
F
方法B
隐藏了F
从中继承的虚方法A
。由于new F
in B
具有私有访问权限,因此其范围仅包括类主体B
且未扩展到C
。因此,允许F
in 的声明C
覆盖F
继承的A
。
密封方法
当实例方法声明包含sealed
修饰符时,该方法被称为密封方法。如果实例方法声明包含 sealed
修饰符,则它还必须包含override
修饰符。使用sealed
修饰符可防止派生类进一步覆盖该方法。
在这个例子中
1 using System; 2 3 class A 4 { 5 public virtual void F() { 6 Console.WriteLine("A.F"); 7 } 8 9 public virtual void G() { 10 Console.WriteLine("A.G"); 11 } 12 } 13 14 class B: A 15 { 16 sealed override public void F() { 17 Console.WriteLine("B.F"); 18 } 19 20 override public void G() { 21 Console.WriteLine("B.G"); 22 } 23 } 24 25 class C: B 26 { 27 override public void G() { 28 Console.WriteLine("C.G"); 29 } 30 }
该类B
提供了两种覆盖方法:一种F
具有sealed
修饰符的G
方法和一种不具有修饰符的方法。B
使用密封modifier
防止C
进一步超越F
。
抽象方法
当实例方法声明包含abstract
修饰符时,该方法被称为抽象方法。虽然抽象方法也隐式地也是虚方法,但它不能具有修饰符virtual
。
抽象方法声明引入了新的虚方法,但未提供该方法的实现。相反,非抽象派生类需要通过覆盖该方法来提供自己的实现。因为抽象方法没有提供实际的实现,所以抽象方法的method_body只包含一个分号。
抽象方法声明仅允许在抽象类(抽象类)中使用。
在这个例子中
1 public abstract class Shape 2 { 3 public abstract void Paint(Graphics g, Rectangle r); 4 } 5 6 public class Ellipse: Shape 7 { 8 public override void Paint(Graphics g, Rectangle r) { 9 g.DrawEllipse(r); 10 } 11 } 12 13 public class Box: Shape 14 { 15 public override void Paint(Graphics g, Rectangle r) { 16 g.DrawRect(r); 17 } 18 }
Shape
类定义了一个几何形状对象可以画本身的抽象概念。该Paint
方法是抽象的,因为没有有意义的默认实现。在Ellipse
和Box
类是具体Shape
实现。因为这些类是非抽象的,所以它们需要覆盖该Paint
方法并提供实际的实现。
base_access(Base访问)引用抽象方法是编译时错误。在这个例子中
1 abstract class A 2 { 3 public abstract void F(); 4 } 5 6 class B: A 7 { 8 public override void F() { 9 base.F(); // Error, base.F is abstract 10 } 11 }
报告base.F()
调用的编译时错误,因为它引用了一个抽象方法。
允许抽象方法声明覆盖虚方法。这允许抽象类强制在派生类中重新实现该方法,并使该方法的原始实现不可用。在这个例子中
1 using System; 2 3 class A 4 { 5 public virtual void F() { 6 Console.WriteLine("A.F"); 7 } 8 } 9 10 abstract class B: A 11 { 12 public abstract override void F(); 13 } 14 15 class C: B 16 { 17 public override void F() { 18 Console.WriteLine("C.F"); 19 } 20 }
class A
声明一个虚方法,类B
用抽象方法覆盖此方法,类C
重写抽象方法以提供自己的实现。
外部方法
当方法声明包含extern
修饰符时,该方法被称为外部方法。外部方法在外部实现,通常使用C#以外的语言。因为外部方法声明没有提供实际的实现,所以外部方法的method_body只包含一个分号。外部方法可能不是通用的。
该extern
改性剂,通常使用结合一个DllImport
属性(互操作与COM和Win32组件),从而允许通过的DLL(动态链接库)来实现外部的方法。执行环境可以支持其他机制,由此可以提供外部方法的实现。
当外部方法包含DllImport
属性时,方法声明还必须包含static
修饰符。此示例演示了extern
修饰符和DllImport
属性的用法:
1 using System.Text; 2 using System.Security.Permissions; 3 using System.Runtime.InteropServices; 4 5 class Path 6 { 7 [DllImport("kernel32", SetLastError=true)] 8 static extern bool CreateDirectory(string name, SecurityAttribute sa); 9 10 [DllImport("kernel32", SetLastError=true)] 11 static extern bool RemoveDirectory(string name); 12 13 [DllImport("kernel32", SetLastError=true)] 14 static extern int GetCurrentDirectory(int bufSize, StringBuilder buf); 15 16 [DllImport("kernel32", SetLastError=true)] 17 static extern bool SetCurrentDirectory(string name); 18 }
部分方法(回顾)
当方法声明包含partial
修饰符时,该方法被称为部分方法。部分方法只能声明为部分类型(部分类型)的成员,并且受到许多限制。局部方法进一步描述于局部的方法。
扩展方法
当方法的第一个参数包含this
修饰符时,该方法被称为扩展方法。扩展方法只能在非泛型的非嵌套静态类中声明。扩展方法的第一个参数除了之外不能有任何修饰符this
,参数类型不能是指针类型。
以下是声明两个扩展方法的静态类的示例:
1 public static class Extensions 2 { 3 public static int ToInt32(this string s) { 4 return Int32.Parse(s); 5 } 6 7 public static T[] Slice<T>(this T[] source, int index, int count) { 8 if (index < 0 || count < 0 || source.Length - index < count) 9 throw new ArgumentException(); 10 T[] result = new T[count]; 11 Array.Copy(source, index, result, 0, count); 12 return result; 13 } 14 }
扩展方法是常规静态方法。此外,在其封闭静态类在范围内的情况下,可以使用实例方法调用语法(扩展方法调用),使用接收器表达式作为第一个参数来调用扩展方法。
以下程序使用上面声明的扩展方法:
1 static class Program 2 { 3 static void Main() { 4 string[] strings = { "1", "22", "333", "4444" }; 5 foreach (string s in strings.Slice(1, 2)) { 6 Console.WriteLine(s.ToInt32()); 7 } 8 } 9 }
该Slice
方法可用string[]
,并且该ToInt32
方法可用string
,因为它们已被声明为扩展方法。使用普通的静态方法调用,程序的含义与以下内容相同:
1 static class Program 2 { 3 static void Main() { 4 string[] strings = { "1", "22", "333", "4444" }; 5 foreach (string s in Extensions.Slice(strings, 1, 2)) { 6 Console.WriteLine(Extensions.ToInt32(s)); 7 } 8 } 9 }
方法体
方法声明的method_body由块体,表达式主体或分号组成。
方法的结果类型是void
返回类型void
,或者方法是异步还是返回类型System.Threading.Tasks.Task
。否则,非异步方法的结果类型是其返回类型,而返回类型System.Threading.Tasks.Task<T>
为async方法的结果类型为T
。
当方法具有void
结果类型和块体时,不允许块中的return
语句(返回语句)指定表达式。如果void方法的块的执行正常完成(即,控制流出方法体的末尾),则该方法只返回其当前调用者。
当方法具有void
结果和表达式主体时,表达式E
必须是statement_expression,并且主体完全等同于表单的块主体{ E; }
。
当方法具有非void结果类型和块体时,块中的每个return
语句都必须指定可隐式转换为结果类型的表达式。必须无法访问值返回方法的块体的端点。换句话说,在具有块体的值返回方法中,不允许控制流出方法体的末端。
当方法具有非void结果类型和表达式主体时,表达式必须可隐式转换为结果类型,并且主体完全等效于表单的块主体{ return E; }
。
在这个例子中
1 class A 2 { 3 public int F() {} // Error, return value required 4 5 public int G() { 6 return 1; 7 } 8 9 public int H(bool b) { 10 if (b) { 11 return 1; 12 } 13 else { 14 return 0; 15 } 16 } 17 18 public int I(bool b) => b ? 1 : 0; 19 }
返回值的F
方法会导致编译时错误,因为控制可以从方法体的末尾流出。该G
和H
,因为所有可能的执行路径中的一个指定返回值的return语句结束的方法是正确的。该I
方法是正确的,因为它的主体等同于一个语句块,其中只有一个return语句。
方法重载
方法重载决策规则在类型推断中描述。
属性
属性是提供访问对象或类的特性的部件。属性的示例包括字符串的长度,字体的大小,窗口的标题,客户的名称等。属性是字段的自然扩展 - 两者都是具有关联类型的命名成员,访问字段和属性的语法是相同的。但是,与字段不同,属性不表示存储位置。相反,属性具有访问器,用于指定在读取或写入值时要执行的语句。因此,属性提供了一种机制,用于将动作与对象属性的读取和写入相关联; 此外,它们允许计算这些属性。
使用property_declaration声明属性:
1 property_declaration 2 : attributes? property_modifier* type member_name property_body 3 ; 4 5 property_modifier 6 : 'new' 7 | 'public' 8 | 'protected' 9 | 'internal' 10 | 'private' 11 | 'static' 12 | 'virtual' 13 | 'sealed' 14 | 'override' 15 | 'abstract' 16 | 'extern' 17 | property_modifier_unsafe 18 ; 19 20 property_body 21 : '{' accessor_declarations '}' property_initializer? 22 | '=>' expression ';' 23 ; 24 25 property_initializer 26 : '=' variable_initializer ';' 27 ;
一个property_declaration可以包括一组属性(属性)和四个访问修饰符的有效组合(访问修饰符),在new
(new修饰符), static
(静态和实例方法), virtual
(虚方法), override
(覆盖方法), sealed
(密封方法),abstract
(抽象方法)和extern
(外部方法)修饰符。
对于有效的修饰符组合,属性声明遵循与方法声明(方法)相同的规则。
该类型属性声明指定由该声明引入的属性的类型和MEMBER_NAME指定属性的名称。除非属性是显式接口成员实现,否则member_name只是一个标识符。对于显式接口成员实现(显式接口成员实现),member_name由interface_type后跟“ .
”和标识符组成。
属性的类型必须至少与属性本身一样可访问(可访问性约束)。
property_body既可以由一个的存取主体或表达体。在访问器主体中, accessor_declarations(必须包含在“ {
”和“ }
”标记中)声明属性的访问器(访问器)。访问器指定与读取和写入属性相关联的可执行语句。
由=>
后跟表达式 E
和分号组成的表达式主体与语句主体完全等效{ get { return E; } }
,因此只能用于指定getter-only属性,其中getter的结果由单个表达式给出。
property_initializer可以仅给出一个自动实现的属性(自动实现的属性),并使得与由给定的值这样的特性的基础字段的初始化表达。
即使访问属性的语法与字段的语法相同,属性也不会归类为变量。因此,不能将属性传递作为ref
或out
参数。
当属性声明包含extern
修饰符时,该属性称为外部属性。由于外部属性声明不提供实际实现,因此每个accessor_declarations都包含一个分号。
静态和实例属性
当属性声明包含static
修饰符时,该属性称为静态属性。如果不存在static
修饰符,则称该属性为实例属性。
静态属性与特定实例无关,并且this
在静态属性的访问器中引用它是编译时错误。
实例属性与类的给定实例相关联,并且该实例可以在该属性的访问器中作为this
(此访问)进行访问。
当在表单的member_access(成员访问)中引用属性时E.M
,如果M
是静态属性,则E
必须表示包含的类型M
,如果M
是实例属性,则E必须表示包含类型的实例M
。
静态和实例成员之间的差异中进一步讨论了静态和实例成员。
访问器
属性的accessor_declarations指定与读取和写入该属性相关联的可执行语句。
1 accessor_declarations 2 : get_accessor_declaration set_accessor_declaration? 3 | set_accessor_declaration get_accessor_declaration? 4 ; 5 6 get_accessor_declaration 7 : attributes? accessor_modifier? 'get' accessor_body 8 ; 9 10 set_accessor_declaration 11 : attributes? accessor_modifier? 'set' accessor_body 12 ; 13 14 accessor_modifier 15 : 'protected' 16 | 'internal' 17 | 'private' 18 | 'protected' 'internal' 19 | 'internal' 'protected' 20 ; 21 22 accessor_body 23 : block 24 | ';' 25 ;
访问器声明包括get_accessor_declaration,set_accessor_declaration或两者。每个访问器声明都包含令牌get
或set
后跟可选的accessor_modifier和accessor_body。
accessor_modifier的使用受以下限制的约束:
- 一个accessor_modifier可能不会在界面或显式接口成员实现中使用。
- 对于没有属性或索引
override
改性剂,accessor_modifier被允许仅当属性或索引同时具有get
和set
存取器,然后仅允许在这些访问器中的一个。 - 对于包含
override
修饰符的属性或索引器,访问器必须匹配被覆盖的访问器的accessor_modifier(如果有)。 - 该accessor_modifier必须声明的可访问性是严格大于属性或索引本身的声明可访问更多的限制。确切地说:
- 如果属性或索引器的声明可访问
public
时,accessor_modifier可以是protected internal
,internal
,protected
,或private
。 - 如果属性或索引器的声明可访问
protected internal
时,accessor_modifier可以是internal
,protected
或private
。 - 如果属性或索引器具有声明的
internal
or的可访问性protected
,则accessor_modifier必须为private
。 - 如果属性或索引器具有声明的可访问性
private
,则不能使用accessor_modifier。
- 如果属性或索引器的声明可访问
为abstract
和extern
性能,所述accessor_body为每个指定的存取器是一个简单的分号。非抽象的非extern属性可以使每个accessor_body都是分号,在这种情况下,它是一个自动实现的属性(自动实现的属性)。自动实现的属性必须至少具有get访问器。对于任何其他非抽象非外部属性的访问器,accessor_body是一个块,它指定在调用相应的访问器时要执行的语句。
get
访问对应于具有属性类型的返回值的参数方法。除了作为赋值的目标之外,当在表达式中引用属性时,将get
调用属性的访问器来计算属性的值(表达式的值)。get
访问器的主体必须符合方法体中描述的值返回方法的规则。特别是,return
访问器主体中的所有语句都get
必须指定一个可隐式转换为属性类型的表达式。此外,get
访问器的端点必须是不可访问的。
set
存取对应于与属性类型的单个值参数和方法void
返回类型。set
始终命名访问器的隐式参数value
。当属性作为赋值(目标引用赋值运算符),或作为操作数++
或--
(后缀增量和减量运算,前缀增量和减量运算),则set
访问被调用,一个参数(其值是的赋值的右侧或++
或--
运算符的操作数)提供新值(简单赋值)。set
访问器的主体必须符合规则void
方法体中描述的方法。特别是,不允许访问器主体中的return
语句set
指定表达式。由于set
访问器隐式地具有名为的参数value
,因此set
对于具有该名称的访问器中的局部变量或常量声明,它是编译时错误。
根据是否存在get
和set
访问器,属性分类如下:
- 包含
get
访问器和set
访问器的属性被称为读写属性。 - 仅具有
get
访问器的属性被称为只读属性。将只读属性作为赋值的目标是编译时错误。 - 仅具有
set
访问器的属性被称为只写属性。除了作为赋值的目标之外,引用表达式中的只写属性是编译时错误。
在这个例子中
1 public class Button: Control 2 { 3 private string caption; 4 5 public string Caption { 6 get { 7 return caption; 8 } 9 set { 10 if (caption != value) { 11 caption = value; 12 Repaint(); 13 } 14 } 15 } 16 17 public override void Paint(Graphics g, Rectangle r) { 18 // Painting code goes here 19 } 20 }
该Button
控制声明了一个公共Caption
属性。属性的get
访问器Caption
返回存储在私有caption
字段中的字符串。所述set
存取器将检查是否新的值是从当前值不同,并且如果是这样,它存储新值并重绘控制。属性通常遵循上面显示的模式:get
访问器只返回存储在私有字段中的值,set
访问器修改该私有字段,然后执行完全更新对象状态所需的任何其他操作。
鉴于Button
上面的类,以下是使用该Caption
属性的示例:
1 Button okButton = new Button(); 2 okButton.Caption = "OK"; // Invokes set accessor 3 string s = okButton.Caption; // Invokes get accessor
这里,set
通过为属性赋值来get
调用访问器,并通过引用表达式中的属性来调用访问器。
的get
和set
属性的访问器是不独特成员,而这是不可能单独声明一个属性的访问。因此,读写属性的两个访问器不可能具有不同的可访问性。这个例子
1 class A 2 { 3 private string name; 4 5 public string Name { // Error, duplicate member name 6 get { return name; } 7 } 8 9 public string Name { // Error, duplicate member name 10 set { name = value; } 11 } 12 }
不声明单个读写属性。相反,它声明了两个具有相同名称的属性,一个是只读的,一个是只写的。由于在同一个类中声明的两个成员不能具有相同的名称,因此该示例会导致发生编译时错误。
当派生类使用与继承属性相同的名称声明属性时,派生属性会隐藏读取和写入的继承属性。在这个例子中
1 class A 2 { 3 public int P { 4 set {...} 5 } 6 } 7 8 class B: A 9 { 10 new public int P { 11 get {...} 12 } 13 }
B继承自A,B隐藏了基类A的P属性。因此,在声明中
1 B b = new B(); 2 b.P = 1; // Error, B.P is read-only 3 ((A)b).P = 1; // Ok, reference to A.P
分配b.P
导致报告编译时错误,因为只读P
属性B
隐藏了只写P
属性A
。但请注意,可以使用强制转换来访问隐藏P
属性。
与公共字段不同,属性提供对象的内部状态与其公共接口之间的分离。考虑这个例子:
1 class Label 2 { 3 private int x, y; 4 private string caption; 5 6 public Label(int x, int y, string caption) { 7 this.x = x; 8 this.y = y; 9 this.caption = caption; 10 } 11 12 public int X { 13 get { return x; } 14 } 15 16 public int Y { 17 get { return y; } 18 } 19 20 public Point Location { 21 get { return new Point(x, y); } 22 } 23 24 public string Caption { 25 get { return caption; } 26 } 27 }
这里,Label
该类使用两个int
字段,x
并y
存储其位置。的位置被公开曝光既作为X
和一个Y
属性并作为Location
属性的类型Point
。如果在将来的版本中Label
,将位置作为Point
内部存储变得更加方便,则可以在不影响类的公共接口的情况下进行更改:
1 class Label 2 { 3 private Point location; 4 private string caption; 5 6 public Label(int x, int y, string caption) { 7 this.location = new Point(x, y); 8 this.caption = caption; 9 } 10 11 public int X { 12 get { return location.x; } 13 } 14 15 public int Y { 16 get { return location.y; } 17 } 18 19 public Point Location { 20 get { return location; } 21 } 22 23 public string Caption { 24 get { return caption; } 25 } 26 }
x
和y
代替了public readonly
场,这本来是不可能做出这样的改变的Label
类。
通过属性公开状态不一定比直接暴露字段效率低。特别是,当属性是非虚拟的并且仅包含少量代码时,执行环境可以用访问器的实际代码替换对访问器的调用。此过程称为内联,它使属性访问与字段访问一样高效,但保留了增加的属性灵活性。
由于调用get
访问器在概念上等同于读取字段的值,因此对于get
访问器来说,具有可观察的副作用的编程风格被认为是错误的。在这个例子中
1 class Counter 2 { 3 private int next; 4 5 public int Next { 6 get { return next++; } 7 } 8 }
Next
属性的值取决于先前访问过该属性的次数。因此,访问属性会产生可观察的副作用,而属性应该作为方法实现。
访问器的“无副作用”约定get
并不意味着get
应始终编写访问器以简单地返回存储在字段中的值。实际上,get
访问器通常通过访问多个字段或调用方法来计算属性的值。但是,正确设计的get
访问器不会执行任何导致对象状态发生可观察更改的操作。
属性可用于延迟资源的初始化,直到它首次被引用。例如:
1 using System.IO; 2 3 public class Console 4 { 5 private static TextReader reader; 6 private static TextWriter writer; 7 private static TextWriter error; 8 9 public static TextReader In { 10 get { 11 if (reader == null) { 12 reader = new StreamReader(Console.OpenStandardInput()); 13 } 14 return reader; 15 } 16 } 17 18 public static TextWriter Out { 19 get { 20 if (writer == null) { 21 writer = new StreamWriter(Console.OpenStandardOutput()); 22 } 23 return writer; 24 } 25 } 26 27 public static TextWriter Error { 28 get { 29 if (error == null) { 30 error = new StreamWriter(Console.OpenStandardError()); 31 } 32 return error; 33 } 34 } 35 }
Console
类包含三个属性,In
,Out
,和Error
,分别表示所述标准输入,输出,和错误的设备。通过将这些成员公开为属性,Console
该类可以延迟它们的初始化,直到它们被实际使用。例如,在第一次引用Out
属性时,如
Console.Out.WriteLine("hello, world");
TextWriter
创建输出设备的基础。但是,如果应用程序没有引用In
和Error
属性,则不会为这些设备创建任何对象。
自动实现的属性
自动实现的属性(或简称auto-property)是一个非抽象的非extern属性,只有分号的访问器体。自动属性必须具有get访问器,并且可以选择具有set访问器。
将属性指定为自动实现的属性时,将为该属性自动提供隐藏的后备字段,并且实现访问器以读取和写入该后备字段。如果auto-property没有set访问器,则考虑后备字段readonly
(Readonly fields)。就像一个readonly
字段一样,也可以在封闭类的构造函数体中指定一个仅具有getter的自动属性。这样的赋值直接分配给属性的只读后备字段。
auto-property可以选择性地具有property_initializer,它作为variable_initializer(Variable initializers)直接应用于支持字段。
以下示例:
1 public class Point { 2 public int X { get; set; } = 0; 3 public int Y { get; set; } = 0; 4 }
相当于以下声明:
1 public class Point { 2 private int __x = 0; 3 private int __y = 0; 4 public int X { get { return __x; } set { __x = value; } } 5 public int Y { get { return __y; } set { __y = value; } } 6 }
以下示例:
1 public class ReadOnlyPoint 2 { 3 public int X { get; } 4 public int Y { get; } 5 public ReadOnlyPoint(int x, int y) { X = x; Y = y; } 6 }
相当于以下声明:
1 public class ReadOnlyPoint 2 { 3 private readonly int __x; 4 private readonly int __y; 5 public int X { get { return __x; } } 6 public int Y { get { return __y; } } 7 public ReadOnlyPoint(int x, int y) { __x = x; __y = y; } 8 }
请注意,只读字段的赋值是合法的,因为它们出现在构造函数中。
无障碍
如果一个存取具有accessor_modifier,可访问域(辅助功能域的访问的)使用的声明可访问确定accessor_modifier。如果访问器没有accessor_modifier,则访问器的可访问域是根据声明的属性或索引器的可访问性来确定的。
accessor_modifier的存在决不会影响成员查找(操作符)或重载解析(重载解析)。无论访问的上下文如何,属性或索引器上的修饰符始终确定绑定到哪个属性或索引器。
选择特定属性或索引器后,所涉及的特定访问器的可访问域用于确定该用法是否有效:
- 如果用法是值(表达式的值),则
get
访问器必须存在且可访问。 - 如果用法是简单赋值(简单赋值)的目标,则
set
访问器必须存在且可访问。 - 如果用法是复合赋值(复合赋值)的目标,或者作为
++
或--
运算符(函数成员 .9,调用表达式)的目标,则get
访问器和set
访问器都必须存在且可访问。
在以下示例中,属性A.Text
由属性隐藏B.Text
,即使在仅set
调用访问器的上下文中也是如此。相反,B.Count
类不能访问该属性M
,因此使用可访问属性A.Count
。
1 class A 2 { 3 public string Text { 4 get { return "hello"; } 5 set { } 6 } 7 8 public int Count { 9 get { return 5; } 10 set { } 11 } 12 } 13 14 class B: A 15 { 16 private string text = "goodbye"; 17 private int count = 0; 18 19 new public string Text { 20 get { return text; } 21 protected set { text = value; } 22 } 23 24 new protected int Count { 25 get { return count; } 26 set { count = value; } 27 } 28 } 29 30 class M 31 { 32 static void Main() { 33 B b = new B(); 34 b.Count = 12; // Calls A.Count set accessor 35 int i = b.Count; // Calls A.Count get accessor 36 b.Text = "howdy"; // Error, B.Text set accessor not accessible 37 string s = b.Text; // Calls B.Text get accessor 38 } 39 }
用于实现接口的访问器可能没有accessor_modifier。如果只使用一个访问器来实现接口,则可以使用accessor_modifier声明另一个访问器:
1 public interface I 2 { 3 string Prop { get; } 4 } 5 6 public class C: I 7 { 8 public Prop { 9 get { return "April"; } // Must not have a modifier here 10 internal set {...} // Ok, because I.Prop has no set accessor 11 } 12 }
虚拟,密封,覆盖和抽象属性访问器
一个virtual
属性声明指定属性的访问是虚拟的。该virtual
修改适用于两个访问的读写性能,这是不可能只有一个访问一个读写属性是虚拟的。
一个abstract
属性声明指定属性的访问是虚拟的,但不提供实际实现的访问器。相反,非抽象派生类需要通过覆盖属性来为访问器提供自己的实现。因为抽象属性声明的访问器没有提供实际的实现,所以它的accessor_body只包含一个分号。
包含abstract
和override
修饰符的属性声明指定该属性是抽象的并覆盖基本属性。这种财产的访问器也是抽象的。
抽象属性声明仅允许在抽象类(抽象类)中。通过包含指定override
指令的属性声明,可以在派生类中重写继承的虚拟属性的访问器。这被称为重写属性声明。覆盖属性声明不会声明新属性。相反,它只是专门化现有虚拟属性的访问器的实现。
覆盖属性声明必须指定与继承属性完全相同的可访问性修饰符,类型和名称。如果继承的属性只有一个访问器(即,如果继承的属性是只读的或只写的),则覆盖属性必须仅包含该访问器。如果继承的属性包含两个访问器(即,如果继承的属性是读写),则覆盖属性可以包括单个访问器或两个访问器。
覆盖属性声明可以包括sealed
修饰符。使用此修饰符可防止派生类进一步覆盖该属性。密封财产的访问器也是密封的。
除了声明和调用语法的差异外,虚拟,密封,覆盖和抽象访问器的行为与虚拟,密封,覆盖和抽象方法完全相同。具体而言,虚拟方法,覆盖方法,密封方法和抽象方法中描述的规则适用,就像访问器是相应形式的方法一样:
get
访问对应于具有属性类型的返回值和相同的改性剂作为含性的参数方法。set
访问对应于具有属性类型,一个单值参数的方法void
返回类型,和相同的改性剂作为含属性。
在这个例子中
1 abstract class A 2 { 3 int y; 4 5 public virtual int X { 6 get { return 0; } 7 } 8 9 public virtual int Y { 10 get { return y; } 11 set { y = value; } 12 } 13 14 public abstract int Z { get; set; } 15 }
X
是一个虚拟只读属性,Y
是一个虚拟读写属性,Z
是一个抽象的读写属性。因为Z
是抽象的,所以包含类A
也必须声明为abstract。
派生自的类A
如下所示:
1 class B: A 2 { 3 int z; 4 5 public override int X { 6 get { return base.X + 1; } 7 } 8 9 public override int Y { 10 set { base.Y = value < 0? 0: value; } 11 } 12 13 public override int Z { 14 get { return z; } 15 set { z = value; } 16 } 17 }
在此,所谓的声明X
,Y
以及Z
被重写属性声明。每个属性声明都与相应的继承属性的可访问性修饰符,类型和名称完全匹配。该get
访问器X
和set
访问器Y
使用base
关键字来访问继承的存取。Z
覆盖两个抽象访问器的声明- 因此,没有未完成的抽象函数成员B
,并且B
被允许是非抽象类。
当属性声明为a时override
,重写代码必须可以访问任何被覆盖的访问器。此外,属性或索引器本身以及访问器的声明可访问性必须与重写的成员和访问器的可访问性相匹配。例如:
1 public class B 2 { 3 public virtual int P { 4 protected set {...} 5 get {...} 6 } 7 } 8 9 public class D: B 10 { 11 public override int P { 12 protected set {...} // Must specify protected here 13 get {...} // Must not have a modifier here 14 } 15 }
事件
一个事件是使对象或类,以提供通知的成员。客户端可以通过提供事件处理程序来附加事件的可执行代码。
事件使用event_declaration声明:
1 event_declaration 2 : attributes? event_modifier* 'event' type variable_declarators ';' 3 | attributes? event_modifier* 'event' type member_name '{' event_accessor_declarations '}' 4 ; 5 6 event_modifier 7 : 'new' 8 | 'public' 9 | 'protected' 10 | 'internal' 11 | 'private' 12 | 'static' 13 | 'virtual' 14 | 'sealed' 15 | 'override' 16 | 'abstract' 17 | 'extern' 18 | event_modifier_unsafe 19 ; 20 21 event_accessor_declarations 22 : add_accessor_declaration remove_accessor_declaration 23 | remove_accessor_declaration add_accessor_declaration 24 ; 25 26 add_accessor_declaration 27 : attributes? 'add' block 28 ; 29 30 remove_accessor_declaration 31 : attributes? 'remove' block 32 ;
一个event_declaration可以包括一组属性(属性)和四个访问修饰符的有效组合(访问修饰符),在new
(new修饰符), static
(静态和实例方法), virtual
(虚方法), override
(覆盖方法), sealed
(密封方法),abstract
(抽象方法)和extern
(外部方法)修饰符。
对于有效的修饰符组合,事件声明遵循与方法声明(方法)相同的规则。
事件声明的类型必须是delegate_type(引用类型),并且该delegate_type必须至少与事件本身一样可访问(可访问性约束)。
事件声明可以包括event_accessor_declarations。但是,如果不是,对于非外部非抽象事件,编译器会自动提供它们(类似字段的事件); 对于外部事件,访问器是外部提供的。
省略event_accessor_declarations的事件声明为每个variable_declarator定义一个或多个事件。属性和修饰符适用于由此类event_declaration声明的所有成员。
event_declaration包含abstract
修饰符和大括号分隔的event_accessor_declarations是编译时错误。
当事件声明包含extern
修饰符时,该事件被称为外部事件。由于外部事件声明不提供实际实现,因此包含extern
修饰符和event_accessor_declarations是错误的。
这是一个编译时错误variable_declarator事件声明的有abstract
或external
改性剂包括variable_initializer。
事件可以用作+=
和-=
运算符的左手操作数(事件分配)。这些运算符分别用于将事件处理程序附加到事件处理程序或从事件中删除事件处理程序,并且事件的访问修饰符控制允许此类操作的上下文。
由于+=
并且-=
是在声明事件的类型之外的事件上允许的唯一操作,外部代码可以添加和删除事件的处理程序,但不能以任何其他方式获取或修改事件处理程序的基础列表。
在表单的操作中,x += y
或者x -= y
,当x
一个事件和引用发生在包含声明的类型之外时x
,操作的结果具有类型void
(而不是具有类型x
,具有x
赋值后的值) 。此规则禁止外部代码间接检查事件的基础委托。
以下示例显示事件处理程序如何附加到Button
类的实例:
1 public delegate void EventHandler(object sender, EventArgs e); 2 3 public class Button: Control 4 { 5 public event EventHandler Click; 6 } 7 8 public class LoginDialog: Form 9 { 10 Button OkButton; 11 Button CancelButton; 12 13 public LoginDialog() { 14 OkButton = new Button(...); 15 OkButton.Click += new EventHandler(OkButtonClick); 16 CancelButton = new Button(...); 17 CancelButton.Click += new EventHandler(CancelButtonClick); 18 } 19 20 void OkButtonClick(object sender, EventArgs e) { 21 // Handle OkButton.Click event 22 } 23 24 void CancelButtonClick(object sender, EventArgs e) { 25 // Handle CancelButton.Click event 26 } 27 }
这里,LoginDialog
实例构造函数创建两个Button
实例并将事件处理程序附加到Click
事件。
类似场地的事件
在包含事件声明的类或结构的程序文本中,可以使用某些事件,如字段。要以这种方式使用,事件不能是abstract
或者extern
,并且不得明确包含event_accessor_declarations。这样的事件可以在允许字段的任何上下文中使用。该字段包含一个委托(Delegates),它引用已添加到事件的事件处理程序列表。如果未添加事件处理程序,则该字段包含null
。
在这个例子中
1 public delegate void EventHandler(object sender, EventArgs e); 2 3 public class Button: Control 4 { 5 public event EventHandler Click; 6 7 protected void OnClick(EventArgs e) { 8 if (Click != null) Click(this, e); 9 } 10 11 public void Reset() { 12 Click = null; 13 } 14 }
Click
用作类中的字段Button
。如示例所示,可以在委托调用表达式中检查,修改和使用该字段。该类中的OnClick
方法Button
“引发” Click
事件。提出事件的概念恰好等同于调用事件所代表的委托 - 因此,没有用于引发事件的特殊语言结构。请注意,委托调用之前是一个检查,以确保委托非空。
在Button
类的声明之外,Click
成员只能在+=
和-=
运算符的左侧使用,如
b.Click += new EventHandler(...);
它将一个委托附加到Click
事件的调用列表中,并且
b.Click -= new EventHandler(...);
从Click事件的调用列表中删除委托。
编译类似字段的事件时,编译器会自动创建存储以保存委托,并为事件创建访问器,以便向委托字段添加或删除事件处理程序。添加和删除操作是线程安全的,并且可以(但不是必须)在实例事件的包含对象上保持锁定(锁定语句),或者在类型对象(匿名对象创建表达式)中执行静态事件。
因此,表单的实例事件声明:
1 class X 2 { 3 public event D Ev; 4 }
将被编译为相当于:
1 class X 2 { 3 private D __Ev; // field to hold the delegate 4 5 public event D Ev { 6 add { 7 /* add the delegate in a thread safe way */ 8 } 9 10 remove { 11 /* remove the delegate in a thread safe way */ 12 } 13 } 14 }
在类中X
,对和运算符Ev
左侧的引用会导致调用add和remove访问器。所有其他引用都被编译为引用隐藏字段(成员访问)。名称“ ”是任意的; 隐藏字段可以有任何名称或根本没有名称。+=
-=
Ev
__Ev
__Ev
事件访问器
事件声明通常会省略event_accessor_declarations,如上Button
例所示。这样做的一种情况涉及每个事件一个字段的存储成本是不可接受的情况。在这种情况下,类可以包括event_accessor_declarations并使用私有机制来存储事件处理程序列表。
事件的event_accessor_declarations指定与添加和删除事件处理程序关联的可执行语句。
访问器声明包含add_accessor_declaration和remove_accessor_declaration。每个访问器声明都包含令牌add
或remove
后跟块。与add_accessor_declaration关联的块指定在添加事件处理程序时要执行的语句,与remove_accessor_declaration关联的块指定在删除事件处理程序时要执行的语句。
每个add_accessor_declaration和remove_accessor_declaration对应于具有事件类型的单个值参数和void
返回类型的方法。命名事件访问器的隐式参数value
。在事件分配中使用事件时,将使用相应的事件访问器。具体来说,如果赋值运算符是+=
使用添加访问器,并且如果赋值运算符是,-=
则使用删除访问器。在任何一种情况下,赋值运算符的右侧操作数都用作事件访问器的参数。add_accessor_declaration或remove_accessor_declaration的块必须符合规则void
方法体中描述的方法。特别是,return
不允许在这样的块中的语句指定表达式。
由于事件访问器隐式地具有名为的参数value
,因此对于在事件访问器中声明的具有该名称的局部变量或常量,它是编译时错误。
在这个例子中
1 class Control: Component 2 { 3 // Unique keys for events 4 static readonly object mouseDownEventKey = new object(); 5 static readonly object mouseUpEventKey = new object(); 6 7 // Return event handler associated with key 8 protected Delegate GetEventHandler(object key) {...} 9 10 // Add event handler associated with key 11 protected void AddEventHandler(object key, Delegate handler) {...} 12 13 // Remove event handler associated with key 14 protected void RemoveEventHandler(object key, Delegate handler) {...} 15 16 // MouseDown event 17 public event MouseEventHandler MouseDown { 18 add { AddEventHandler(mouseDownEventKey, value); } 19 remove { RemoveEventHandler(mouseDownEventKey, value); } 20 } 21 22 // MouseUp event 23 public event MouseEventHandler MouseUp { 24 add { AddEventHandler(mouseUpEventKey, value); } 25 remove { RemoveEventHandler(mouseUpEventKey, value); } 26 } 27 28 // Invoke the MouseUp event 29 protected void OnMouseUp(MouseEventArgs args) { 30 MouseEventHandler handler; 31 handler = (MouseEventHandler)GetEventHandler(mouseUpEventKey); 32 if (handler != null) 33 handler(this, args); 34 } 35 }
Control
类用于实现事件的内部存储机制。该AddEventHandler
方法将委托值与键相关联,该GetEventHandler
方法返回当前与键关联的委托,该RemoveEventHandler
方法将委托移除为指定事件的事件处理程序。据推测,底层存储机制被设计成使得null
委托值与密钥相关联没有成本,因此未处理的事件不消耗存储。
静态和实例事件
当事件声明包含static
修饰符时,该事件被称为静态事件。当不存在static
修饰符时,该事件被称为实例事件。
静态事件与特定实例无关,并且this
在静态事件的访问器中引用是编译时错误。
实例事件与类的给定实例相关联,并且此实例可以在该事件的访问器中作为this
(此访问)进行访问。
当在表单的member_access(成员访问)中引用事件时E.M
,如果M
是静态事件,则E
必须表示包含的类型M
,如果M
是实例事件,则E必须表示包含类型的实例M
。
静态和实例成员之间的差异中进一步讨论了静态和实例成员。
虚拟,密封,覆盖和抽象事件访问器
一个virtual
事件声明指定该事件的访问器是虚的。该virtual
修改适用于事件的两个访问。
一个abstract
事件声明指定事件的访问器是虚拟的,但不提供实际实现的访问器。相反,非抽象派生类需要通过覆盖事件为访问者提供自己的实现。因为抽象事件声明不提供实际实现,所以它不能提供大括号分隔的event_accessor_declarations。
包含abstract
和override
修饰符的事件声明指定事件是抽象的并覆盖基本事件。这种事件的访问者也是抽象的。
抽象事件声明仅允许在抽象类(抽象类)中使用。
通过包含指定override
修饰符的事件声明,可以在派生类中重写继承的虚拟事件的访问器。这被称为重写事件声明。重写事件声明不会声明新事件。相反,它只是专门化现有虚拟事件的访问器的实现。
重写事件声明必须指定与重写事件完全相同的辅助功能修饰符,类型和名称。
重写事件声明可以包括sealed
修饰符。使用此修饰符可防止派生类进一步覆盖事件。密封事件的访问者也是密封的。
包含new
修饰符的重写事件声明是编译时错误。
除了声明和调用语法的差异外,虚拟,密封,覆盖和抽象访问器的行为与虚拟,密封,覆盖和抽象方法完全相同。具体而言,虚拟方法,覆盖方法,密封方法和抽象方法中描述的规则适用,就像访问器是相应形式的方法一样。每个访问器对应一个方法,该方法具有事件类型的单个值参数,void
返回类型以及与包含事件相同的修饰符。
索引
一个索引器是使得以相同的方式作为数组要索引的对象的构件。索引器使用indexer_declaration声明:
1 indexer_declaration 2 : attributes? indexer_modifier* indexer_declarator indexer_body 3 ; 4 5 indexer_modifier 6 : 'new' 7 | 'public' 8 | 'protected' 9 | 'internal' 10 | 'private' 11 | 'virtual' 12 | 'sealed' 13 | 'override' 14 | 'abstract' 15 | 'extern' 16 | indexer_modifier_unsafe 17 ; 18 19 indexer_declarator 20 : type 'this' '[' formal_parameter_list ']' 21 | type interface_type '.' 'this' '[' formal_parameter_list ']' 22 ; 23 24 indexer_body 25 : '{' accessor_declarations '}' 26 | '=>' expression ';' 27 ;
一个indexer_declaration可以包括一组属性(属性)和四个访问修饰符(的有效组合访问修饰符),在new
(new修饰符), virtual
(虚方法), override
(覆盖方法), sealed
(密封方法), abstract
(抽象方法)和extern
(外部方法)修饰符。
对于有效的修饰符组合,索引器声明与方法声明(方法)的规则相同,唯一的例外是索引器声明中不允许使用static修饰符。
的改性剂virtual
,override
以及abstract
相同,除了在一种情况下相互排斥的。的abstract
和override
改性剂可以一起使用,使得一个抽象的索引可以覆盖一个虚拟的。
所述类型的分度器的声明指定由该声明引入的索引的元素类型。除非索引器是显式接口成员实现,否则类型后跟关键字this
。对于显式接口成员实现,类型后跟interface_type,“ .
”和关键字this
。与其他成员不同,索引器没有用户定义的名称。
该formal_parameter_list指定索引的参数。索引器的形式参数列表对应于方法(方法参数)的形式参数列表,但必须至少指定一个参数,并且不允许使用ref
和out
参数修饰符。
所述类型的分度器,并且每个所引用的类型的formal_parameter_list必须至少与索引器本身(如可访问辅助约束)。
一个indexer_body既可以由一个的存取主体或表达体。在访问器主体中,accessor_declarations(必须包含在“ {
”和“ }
”标记中)声明属性的访问者(访问者)。访问器指定与读取和写入属性相关联的可执行语句。
由“ =>
”后跟表达式E
和分号组成的表达式主体与语句主体完全等效{ get { return E; } }
,因此只能用于指定getter-only索引器,其中getter的结果由单个表达式给出。
即使访问索引器元素的语法与数组元素的语法相同,索引器元素也不会被归类为变量。因此,就不可能通过一个索引元素作为ref
或out
参数。
索引器的形式参数列表定义索引器的签名(签名和重载)。具体而言,索引器的签名由其形式参数的数量和类型组成。形式参数的元素类型和名称不是索引器签名的一部分。
索引器的签名必须与同一类中声明的所有其他索引器的签名不同。
索引器和属性在概念上非常相似,但在以下方面有所不同:
- 属性由其名称标识,而索引器由其签名标识。
- 通过simple_name(简单名称)或member_access(成员访问)访问属性,而通过element_access(索引器访问)访问索引器元素。
- 属性可以是
static
成员,而索引器始终是实例成员。 get
一个属性的访问对应于不带参数的方法,而get
一个索引的访问对应于具有相同的形式参数列表作为索引的方法。set
一个属性的访问对应于与命名的单个参数的方法value
,而set
一个索引的访问对应于具有相同的形式参数列表作为索引,加命名的附加参数的方法value
。- 索引器访问器声明与索引器参数同名的局部变量是编译时错误。
- 在重写属性声明中,使用语法访问继承的属性
base.P
,其中P
是属性名称。在重写索引器声明中,使用语法访问继承的索引器base[E]
,其中E
是逗号分隔的表达式列表。 - 没有“自动实现的索引器”的概念。使用带分号访问器的非抽象非外部索引器是错误的。
除了这些差异之外,访问者和自动实现的属性中定义的所有规则都适用于索引器访问器和属性访问器。
当索引器声明包含extern
修饰符时,索引器被称为外部索引器。由于外部索引器声明不提供实际实现,因此每个accessor_declarations都包含一个分号。
下面的示例声明了一个BitArray
实现索引器的类,用于访问位数组中的各个位。
1 using System; 2 3 class BitArray 4 { 5 int[] bits; 6 int length; 7 8 public BitArray(int length) { 9 if (length < 0) throw new ArgumentException(); 10 bits = new int[((length - 1) >> 5) + 1]; 11 this.length = length; 12 } 13 14 public int Length { 15 get { return length; } 16 } 17 18 public bool this[int index] { 19 get { 20 if (index < 0 || index >= length) { 21 throw new IndexOutOfRangeException(); 22 } 23 return (bits[index >> 5] & 1 << index) != 0; 24 } 25 set { 26 if (index < 0 || index >= length) { 27 throw new IndexOutOfRangeException(); 28 } 29 if (value) { 30 bits[index >> 5] |= 1 << index; 31 } 32 else { 33 bits[index >> 5] &= ~(1 << index); 34 } 35 } 36 } 37 }
BitArray
该类的实例消耗的内存比对应的少得多bool[]
(因为前者的每个值只占一位而不是后者的一个字节),但它允许与a相同的操作bool[]
。
下面的CountPrimes
类使用a BitArray
和经典的“筛子”算法来计算1和给定最大值之间的素数:
1 class CountPrimes 2 { 3 static int Count(int max) { 4 BitArray flags = new BitArray(max + 1); 5 int count = 1; 6 for (int i = 2; i <= max; i++) { 7 if (!flags[i]) { 8 for (int j = i * 2; j <= max; j += i) flags[j] = true; 9 count++; 10 } 11 } 12 return count; 13 } 14 15 static void Main(string[] args) { 16 int max = int.Parse(args[0]); 17 int count = Count(max); 18 Console.WriteLine("Found {0} primes between 1 and {1}", count, max); 19 } 20 }
请注意,访问元素的语法与a的语法BitArray
完全相同bool[]
。
以下示例显示了一个26 * 10网格类,其中包含一个带有两个参数的索引器。第一个参数必须是AZ范围内的大写或小写字母,第二个参数必须是0-9范围内的整数。
1 using System; 2 3 class Grid 4 { 5 const int NumRows = 26; 6 const int NumCols = 10; 7 8 int[,] cells = new int[NumRows, NumCols]; 9 10 public int this[char c, int col] { 11 get { 12 c = Char.ToUpper(c); 13 if (c < 'A' || c > 'Z') { 14 throw new ArgumentException(); 15 } 16 if (col < 0 || col >= NumCols) { 17 throw new IndexOutOfRangeException(); 18 } 19 return cells[c - 'A', col]; 20 } 21 22 set { 23 c = Char.ToUpper(c); 24 if (c < 'A' || c > 'Z') { 25 throw new ArgumentException(); 26 } 27 if (col < 0 || col >= NumCols) { 28 throw new IndexOutOfRangeException(); 29 } 30 cells[c - 'A', col] = value; 31 } 32 } 33 }
索引器重载
索引器重载决策规则在类型推断中描述。
运算符
运算符是定义可以适用于该类的实例的表达式运算符的含义的构件。运算符使用operator_declaration声明:
1 operator_declaration 2 : attributes? operator_modifier+ operator_declarator operator_body 3 ; 4 5 operator_modifier 6 : 'public' 7 | 'static' 8 | 'extern' 9 | operator_modifier_unsafe 10 ; 11 12 operator_declarator 13 : unary_operator_declarator 14 | binary_operator_declarator 15 | conversion_operator_declarator 16 ; 17 18 unary_operator_declarator 19 : type 'operator' overloadable_unary_operator '(' type identifier ')' 20 ; 21 22 overloadable_unary_operator 23 : '+' | '-' | '!' | '~' | '++' | '--' | 'true' | 'false' 24 ; 25 26 binary_operator_declarator 27 : type 'operator' overloadable_binary_operator '(' type identifier ',' type identifier ')' 28 ; 29 30 overloadable_binary_operator 31 : '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^' | '<<' 32 | 'right_shift' | '==' | '!=' | '>' | '<' | '>=' | '<=' 33 ; 34 35 conversion_operator_declarator 36 : 'implicit' 'operator' type '(' type identifier ')' 37 | 'explicit' 'operator' type '(' type identifier ')' 38 ; 39 40 operator_body 41 : block 42 | '=>' expression ';' 43 | ';' 44 ;
有三类可重载运算符:一元运算符(一元运算符),二元运算符(二元运算符)和转换运算符(转换运算符)。
所述operator_body或者是分号,一个语句体或表达体。语句体由一个块组成,该块指定调用运算符时要执行的语句。该块必须符合Method body中描述的值返回方法的规则。表达式主体=>
后跟一个表达式和一个分号,表示在调用运算符时要执行的单个表达式。
对于extern
运算符,operator_body只包含一个分号。对于所有其他运算符,operator_body是块体或表达式主体。
以下规则适用于所有运算符声明:
- 运算符声明必须包含a
public
和static
修饰符。 - 运算符的参数必须是值参数(值参数)。对于指定
ref
或out
参数的操作员声明,这是编译时错误。 - 运算符(一元运算符,二元运算符,转换运算符)的签名必须与同一类中声明的所有其他运算符的签名不同。
- 运算符声明中引用的所有类型必须至少与运算符本身一样可访问(可访问性约束)。
- 同一修饰符在运算符声明中多次出现是错误的。
每个运算符类别都会施加其他限制,如以下各节所述。
与其他成员一样,在基类中声明的运算符由派生类继承。因为运算符声明总是要求声明运算符的类或结构参与运算符的签名,所以在派生类中声明的运算符不可能隐藏在基类中声明的运算符。因此,new
在操作员声明中永远不需要修饰符,因此永远不允许使用修饰符。
在一元和二元运算符的其他信息可以发现运算符。
有关转换运算符的更多信息,请参见用户定义的转换。
一元运算符
以下规则适用于一元运算符声明,其中T
表示包含运算符声明的类或结构的实例类型:
- 一元
+
,-
,!
,或~
操作者必须采取类型的单个参数T
或T?
与可以返回任何类型。 - 一元
++
或--
操作者必须采取类型的单个参数T
或T?
并必须返回相同类型或从其派生的类型。 - 一元
true
或false
操作者必须采取类型的单个参数T
或T?
与必须返回类型bool
。
一元运算符的签名由操作者凭证(+
,-
,!
,~
,++
,--
,true
,或false
)和单一形式参数的类型。返回类型不是一元运算符签名的一部分,也不是形式参数的名称。
在true
和false
一元运算符需要成对声明。如果类声明其中一个运算符而不声明另一个运算符,则会发生编译时错误。的true
和false
运算符都在进一步描述用户定义的条件逻辑运算和布尔表达式。
以下示例显示operator ++
了整数向量类的实现和后续用法:
1 public class IntVector 2 { 3 public IntVector(int length) {...} 4 5 public int Length {...} // read-only property 6 7 public int this[int index] {...} // read-write indexer 8 9 public static IntVector operator ++(IntVector iv) { 10 IntVector temp = new IntVector(iv.Length); 11 for (int i = 0; i < iv.Length; i++) 12 temp[i] = iv[i] + 1; 13 return temp; 14 } 15 } 16 17 class Test 18 { 19 static void Main() { 20 IntVector iv1 = new IntVector(4); // vector of 4 x 0 21 IntVector iv2; 22 23 iv2 = iv1++; // iv2 contains 4 x 0, iv1 contains 4 x 1 24 iv2 = ++iv1; // iv2 contains 4 x 2, iv1 contains 4 x 2 25 } 26 }
注意operator方法如何返回通过向操作数添加1而产生的值,就像后缀增量和减量运算符(Postfix递增和递减运算符),以及前缀增量和减量运算符(前缀增量和减量运算符)一样。与C ++不同,此方法无需直接修改其操作数的值。实际上,修改操作数值会违反后缀增量运算符的标准语义。
二元运算符
以下规则适用于二元运算符声明,其中T
表示包含运算符声明的类或结构的实例类型:
- 二元非移位运算符必须带两个参数,其中至少有一个必须具有类型
T
或T?
,并且可以返回任何类型。 - 二元
<<
或>>
运算符必须带两个参数,第一个必须具有类型T
或T?
第二个必须具有类型int
或int?
,并且可以返回任何类型。
二进制运算符的签名由运算符标记(的+
,-
,*
,/
,%
,&
,|
,^
,<<
,>>
,==
,!=
,>
,<
,>=
,或<=
),并且类型两个形参。返回类型和形式参数的名称不是二元运算符签名的一部分。
某些二元运算符需要成对声明。对于一对运算符的每个声明,必须存在该对的另一运算符的匹配声明。当两个运算符声明具有相同的返回类型且每个参数的类型相同时,它们匹配。以下运算符需要成对声明:
operator ==
和operator !=
operator >
和operator <
operator >=
和operator <=
转换运算符
转换运算符声明引入了用户定义的转换(用户定义的转换),它扩充了预定义的隐式和显式转换。
包含implicit
关键字的转换运算符声明引入了用户定义的隐式转换。隐式转换可以在各种情况下发生,包括函数成员调用,强制转换表达式和赋值。这在Implicit转换中进一步描述。
包含explicit
关键字的转换运算符声明引入了用户定义的显式转换。显式转换可以在强制转换表达式中进行,并在显式转换中进一步描述。
转换运算符从源类型(由转换运算符的参数类型指示)转换为目标类型,由转换运算符的返回类型指示。
对于给定的源类型S
和目标类型T
,如果S
或者T
是空类型,让S0
与T0
参考它们的基础类型,否则S0
和T0
是等于S
和T
分别。仅当满足以下所有条件时,才允许类或结构声明从源类型S
到目标类型的转换T
:
S0
并且T0
是不同的类型。- 无论是
S0
或者T0
是在运算符声明发生在类或结构类型。 - 既不是
S0
也不T0
是interface_type。 - 排除用户定义的转换,转换不从存在
S
于T
或从T
到S
。
出于这些规则的目的,与任何类型参数相关联S
或被T
认为是与其他类型没有继承关系的唯一类型,并且忽略对这些类型参数的任何约束。
在这个例子中
1 class C<T> {...} 2 3 class D<T>: C<T> 4 { 5 public static implicit operator C<int>(D<T> value) {...} // Ok 6 public static implicit operator C<string>(D<T> value) {...} // Ok 7 public static implicit operator C<T>(D<T> value) {...} // Error 8 }
前两个操作者声明是允许,因为对于的目的索引器 0.3,T
和int
和string
分别被认为是独特类型没有关系。但是,第三个运算符是一个错误,因为C<T>
它是基类D<T>
。
从第二个规则可以看出,转换运算符必须转换为声明运算符的类或结构类型。例如,可能的是一个类或结构类型C
,以从定义转换C
到int
和从int
到C
,但不能从int
到bool
。
无法直接重新定义预定义的转换。因此,不允许转换运算符转换或转换,object
因为在object
所有其他类型之间已经存在隐式和显式转换。同样,转换的源类型和目标类型都不能是另一个的基类型,因为转换就已经存在了。
但是,可以在泛型类型上声明运算符,对于特定类型参数,可以指定已作为预定义转换存在的转换。在这个例子中
1 struct Convertible<T> 2 { 3 public static implicit operator Convertible<T>(T value) {...} 4 public static explicit operator T(Convertible<T> value) {...} 5 }
当type object
被指定为类型参数时T
,第二个运算符声明已经存在的转换(从任何类型到类型都存在隐式转换,因此也存在显式转换object
)。
如果两种类型之间存在预定义的转换,则忽略这些类型之间的任何用户定义的转换。特别:
- 如果从一个类型到另一个类型存在预定义的隐式转换(隐式转换),则忽略来自的所有用户定义的转换(隐式或显式)。
S
T
S
T
- 如果从类型到类型存在预定义的显式转换(显式转换),则忽略来自的任何用户定义的显式转换。此外:
S
T
S
T
如果T
是一个接口类型,从用户定义的隐式转换S
至T
被忽略。
否则,由用户定义的隐式转换S
到T
仍被认为。
但对于所有类型object
,上述类型声明的运算符Convertible<T>
与预定义的转换不冲突。例如:
1 void F(int i, Convertible<int> n) { 2 i = n; // Error 3 i = (int)n; // User-defined explicit conversion 4 n = i; // User-defined implicit conversion 5 n = (Convertible<int>)i; // User-defined implicit conversion 6 }
但是,对于类型object
,预定义的转换会在所有情况下隐藏用户定义的转换,但只有一个:
1 void F(object o, Convertible<object> n) { 2 o = n; // Pre-defined boxing conversion 3 o = (object)n; // Pre-defined boxing conversion 4 n = o; // User-defined implicit conversion 5 n = (Convertible<object>)o; // Pre-defined unboxing conversion 6 }
用户定义的转换不允许从interface_type转换或转换为interface_type。特别是,此限制确保在转换为interface_type时不会发生用户定义的转换,并且只有在转换的对象实际实现指定的interface_type时,才能成功转换为interface_type。
转换运算符的签名由源类型和目标类型组成。(请注意,这是返回类型参与签名的唯一成员形式。)转换运算符的implicit
或explicit
分类不是运算符签名的一部分。因此,类或结构不能声明具有相同源和目标类型implicit
的explicit
转换运算符。
通常,用户定义的隐式转换应设计为永远不会抛出异常,永远不会丢失信息。如果用户定义的转换可能引起异常(例如,因为源参数超出范围)或信息丢失(例如丢弃高位),则应将该转换定义为显式转换。
在这个例子中
1 using System; 2 3 public struct Digit 4 { 5 byte value; 6 7 public Digit(byte value) { 8 if (value < 0 || value > 9) throw new ArgumentException(); 9 this.value = value; 10 } 11 12 public static implicit operator byte(Digit d) { 13 return d.value; 14 } 15 16 public static explicit operator Digit(byte b) { 17 return new Digit(b); 18 } 19 }
转换为Digit
to byte
是隐式的,因为它从不抛出异常或丢失信息,但转换为byte
to Digit
是显式的,因为Digit
它只能表示a的可能值的子集byte
。
实例构造函数
一个实例构造函数是实现初始化类实例所需操作的成员。使用constructor_declaration声明实例构造函数:
1 constructor_declaration 2 : attributes? constructor_modifier* constructor_declarator constructor_body 3 ; 4 5 constructor_modifier 6 : 'public' 7 | 'protected' 8 | 'internal' 9 | 'private' 10 | 'extern' 11 | constructor_modifier_unsafe 12 ; 13 14 constructor_declarator 15 : identifier '(' formal_parameter_list? ')' constructor_initializer? 16 ; 17 18 constructor_initializer 19 : ':' 'base' '(' argument_list? ')' 20 | ':' 'this' '(' argument_list? ')' 21 ; 22 23 constructor_body 24 : block 25 | ';' 26 ;
constructor_declaration可以包括一组的属性(属性),四个访问修饰符(的有效组合访问改性剂),和extern
(外部方法)改性剂。构造函数声明不允许多次包含相同的修饰符。
该标识符一个的constructor_declarator必须命名其中的实例构造函数被声明的类。如果指定了任何其他名称,则会发生编译时错误。
实例构造函数的可选formal_parameter_list 遵循与方法(方法)的formal_parameter_list相同的规则。形式参数列表定义实例构造函数的签名(签名和重载),并控制进程,由此重载解析(类型推断)在调用中选择特定的实例构造函数。
实例构造函数的formal_parameter_list中引用的每个类型必须至少与构造函数本身一样可访问(可访问性约束)。
可选的constructor_initializer指定在执行此实例构造函数的constructor_body中给出的语句之前要调用的另一个实例构造函数。这在构造函数初始值设定项中进一步描述。
当构造函数声明包含extern
修饰符时,构造函数被称为外部构造函数。由于外部构造函数声明不提供实际实现,因此其constructor_body由分号组成。对于所有其他构造函数,constructor_body由一个块组成,该块指定用于初始化类的新实例的语句。这与具有返回类型(Method body)的实例方法的块完全对应。void
实例构造函数不是继承的。因此,除了在类中实际声明的实例之外,类没有实例构造函数。如果类不包含实例构造函数声明,则会自动提供默认实例构造函数(默认构造函数)。
实例构造函数由object_creation_expression(对象创建表达式)和constructor_initializer调用。
构造函数初始值设定项
所有实例构造函数(除了类的构造函数object
)隐式地包含在constructor_body之前立即调用另一个实例构造函数。隐式调用的构造函数由constructor_initializer确定:
- 表单的实例构造函数初始值设定项
base(argument_list)
或base()
导致直接基类的实例构造函数被调用。使用argument_list(如果存在)和重载决策的重载决策规则选择该构造函数。如果在直接基类中没有声明实例构造函数,则候选实例构造函数集包含直接基类中包含的所有可访问实例构造函数或默认构造函数(默认构造函数)。如果此set为空,或者无法识别单个最佳实例构造函数,则会发生编译时错误。 - 表单的实例构造函数初始值设定项
this(argument-list)
或this()
使类本身的实例构造函数被调用。使用argument_list(如果存在)和重载决策的重载决策规则来选择构造函数。候选实例构造函数集包含在类本身中声明的所有可访问实例构造函数。如果此set为空,或者无法识别单个最佳实例构造函数,则会发生编译时错误。如果实例构造函数声明包含调用构造函数本身的构造函数初始值设定项,则会发生编译时错误。
如果实例构造函数没有构造函数初始值设定项,base()
则隐式提供表单的构造函数初始值设定项。因此,表单的实例构造函数声明
C(...) {...}
完全等同于
C(...): base() {...}
实例构造函数声明的formal_parameter_list给出的参数范围包括该声明的构造函数初始值设定项。因此,允许构造函数初始值设定项访问构造函数的参数。例如:
1 class A 2 { 3 public A(int x, int y) {} 4 } 5 6 class B: A 7 { 8 public B(int x, int y): base(x + y, x - y) {} 9 }
实例构造函数初始值设定项无法访问正在创建的实例。因此this
,在构造函数初始值设定项的参数表达式中引用是编译时错误,因为参数表达式通过simple_name引用任何实例成员的编译时错误。
实例变量初始值设定项
当实例构造函数没有构造函数初始化程序,或者它具有表单的构造函数初始值设定项时base(...)
,该构造函数隐式执行由其类中声明的实例字段的variable_initializer指定的初始化。这对应于在进入构造函数之后和直接调用直接基类构造函数之前立即执行的赋值序列。变量初始值设定项以它们出现在类声明中的文本顺序执行。
构造函数执行
变量初始值设定项转换为赋值语句,这些赋值语句在调用基类实例构造函数之前执行。此排序可确保在执行有权访问该实例的任何语句之前,所有实例字段均由其变量初始值设定项初始化。
举个例子
1 using System; 2 3 class A 4 { 5 public A() { 6 PrintFields(); 7 } 8 9 public virtual void PrintFields() {} 10 } 11 12 class B: A 13 { 14 int x = 1; 15 int y; 16 17 public B() { 18 y = -1; 19 } 20 21 public override void PrintFields() { 22 Console.WriteLine("x = {0}, y = {1}", x, y); 23 } 24 }
当new B()
用于创建实例时B
,会生成以下输出:
x = 1, y = 0
值为x
1,因为变量初始化程序在调用基类实例构造函数之前执行。但是,值为y
0(an的默认值int
),因为y
在基类构造函数返回之前不会执行赋值。
将实例变量初始化器和构造函数初始化器视为在constructor_body之前自动插入的语句很有用。这个例子
1 using System; 2 using System.Collections; 3 4 class A 5 { 6 int x = 1, y = -1, count; 7 8 public A() { 9 count = 0; 10 } 11 12 public A(int n) { 13 count = n; 14 } 15 } 16 17 class B: A 18 { 19 double sqrt2 = Math.Sqrt(2.0); 20 ArrayList items = new ArrayList(100); 21 int max; 22 23 public B(): this(100) { 24 items.Add("default"); 25 } 26 27 public B(int n): base(n - 1) { 28 max = n; 29 } 30 }
包含几个变量初始化器; 它还包含两种形式(base
和this
)的构造函数初始值设定项。该示例对应于下面显示的代码,其中每个注释指示自动插入的语句(用于自动插入的构造函数调用的语法无效,但仅用于说明机制)。
1 using System.Collections; 2 3 class A 4 { 5 int x, y, count; 6 7 public A() { 8 x = 1; // Variable initializer 9 y = -1; // Variable initializer 10 object(); // Invoke object() constructor 11 count = 0; 12 } 13 14 public A(int n) { 15 x = 1; // Variable initializer 16 y = -1; // Variable initializer 17 object(); // Invoke object() constructor 18 count = n; 19 } 20 } 21 22 class B: A 23 { 24 double sqrt2; 25 ArrayList items; 26 int max; 27 28 public B(): this(100) { 29 B(100); // Invoke B(int) constructor 30 items.Add("default"); 31 } 32 33 public B(int n): base(n - 1) { 34 sqrt2 = Math.Sqrt(2.0); // Variable initializer 35 items = new ArrayList(100); // Variable initializer 36 A(n - 1); // Invoke A(int) constructor 37 max = n; 38 } 39 }
默认构造函数
如果类不包含实例构造函数声明,则会自动提供默认实例构造函数。该默认构造函数只是调用直接基类的无参数构造函数。如果类是抽象的,那么默认构造函数的声明可访问性将受到保护。否则,默认构造函数的声明可访问性是公共的。因此,默认构造函数始终是表单
protected C(): base() {}
或者
public C(): base() {}
哪个C
是类的名称。如果重载解析无法确定基类构造函数初始值设定项的唯一最佳候选者,则会发生编译时错误。
在这个例子中
1 class Message 2 { 3 object sender; 4 string text; 5 }
提供了默认构造函数,因为该类不包含实例构造函数声明。因此,这个例子恰好相当于
1 class Message 2 { 3 object sender; 4 string text; 5 6 public Message(): base() {} 7 }
私有实例构造函数
当一个类T
只声明私有实例构造函数时,程序文本之外的类不可能T
派生自T
或直接创建实例T
。因此,如果一个类只包含静态成员而不打算实例化,则添加一个空的私有实例构造函数将阻止实例化。例如:
1 public class Trig 2 { 3 private Trig() {} // Prevent instantiation 4 5 public const double PI = 3.14159265358979323846; 6 7 public static double Sin(double x) {...} 8 public static double Cos(double x) {...} 9 public static double Tan(double x) {...} 10 }
该Trig
阶层群体相关的方法和常数,但不打算被实例化。因此它声明了一个空的私有实例构造函数。必须声明至少一个实例构造函数以禁止自动生成默认构造函数。
可选的实例构造函数参数
this(...)
构造函数初始值设定项的形式通常与重载一起使用,以实现可选的实例构造函数参数。在这个例子中
1 class Text 2 { 3 public Text(): this(0, 0, null) {} 4 5 public Text(int x, int y): this(x, y, null) {} 6 7 public Text(int x, int y, string s) { 8 // Actual constructor implementation 9 } 10 }
前两个实例构造函数仅提供缺少参数的默认值。两者都使用this(...)
构造函数初始化程序来调用第三个实例构造函数,它实际上完成了初始化新实例的工作。效果是可选的构造函数参数:
1 Text t1 = new Text(); // Same as Text(0, 0, null) 2 Text t2 = new Text(5, 10); // Same as Text(5, 10, null) 3 Text t3 = new Text(5, 20, "Hello");
静态构造函数
静态构造是实现初始化的封闭类类型所需操作的构件。静态构造函数使用static_constructor_declaration声明:
1 static_constructor_declaration 2 : attributes? static_constructor_modifiers identifier '(' ')' static_constructor_body 3 ; 4 5 static_constructor_modifiers 6 : 'extern'? 'static' 7 | 'static' 'extern'? 8 | static_constructor_modifiers_unsafe 9 ; 10 11 static_constructor_body 12 : block 13 | ';' 14 ;
static_constructor_declaration可以包括一组的属性(属性)和extern
改性剂(外部方法)。
该标识符一个的static_constructor_declaration必须命名其中静态构造函数被声明的类。如果指定了任何其他名称,则会发生编译时错误。
当静态构造函数声明包含extern
修饰符时,静态构造函数被称为外部静态构造函数。因为外部静态构造函数声明不提供实际实现,所以static_constructor_body由分号组成。对于所有其他静态构造函数声明,static_constructor_body由一个块组成,该块指定要初始化类的语句。这与具有返回类型(Method body)的静态方法的method_body完全对应。void
静态构造函数不是继承的,不能直接调用。
封闭类类型的静态构造函数在给定的应用程序域中最多执行一次。静态构造函数的执行由应用程序域中发生的以下第一个事件触发:
- 创建类类型的实例。
- 引用类类型的任何静态成员。
如果类包含执行开始的Main
方法(Application Startup),则在Main
调用方法之前执行该类的静态构造函数。
要初始化新的封闭类类型,首先要创建该特定封闭类型的一组新静态字段(静态和实例字段)。每个静态字段都初始化为其默认值(默认值)。接下来,对那些静态字段执行静态字段初始化器(静态字段初始化)。最后,执行静态构造函数。
这个例子
1 using System; 2 3 class Test 4 { 5 static void Main() { 6 A.F(); 7 B.F(); 8 } 9 } 10 11 class A 12 { 13 static A() { 14 Console.WriteLine("Init A"); 15 } 16 public static void F() { 17 Console.WriteLine("A.F"); 18 } 19 } 20 21 class B 22 { 23 static B() { 24 Console.WriteLine("Init B"); 25 } 26 public static void F() { 27 Console.WriteLine("B.F"); 28 } 29 }
必须产生输出:
1 Init A 2 A.F 3 Init B 4 B.F
因为A
静态构造函数的执行是由调用触发的A.F
,而B
静态构造函数的执行是由调用触发的B.F
。
可以构造循环依赖关系,允许在其默认值状态下观察具有可变初始值设定项的静态字段。
这个例子
1 using System; 2 3 class A 4 { 5 public static int X; 6 7 static A() { 8 X = B.Y + 1; 9 } 10 } 11 12 class B 13 { 14 public static int Y = A.X + 1; 15 16 static B() {} 17 18 static void Main() { 19 Console.WriteLine("X = {0}, Y = {1}", A.X, B.Y); 20 } 21 }
产生输出
要执行该Main
方法,系统首先B.Y
在类B
的静态构造函数之前运行初始化程序。Y
的初始化程序导致A
运行静态构造函数,因为A.X
引用了它的值。静态构造函数A
又继续计算其值X
,并在此过程中获取默认值Y
,即零。A.X
因此初始化为1.运行A
静态字段初始化程序和静态构造函数的过程然后完成,返回计算初始值Y
,结果变为2。
因为静态构造函数对于每个关闭的构造类类型只执行一次,所以对于在编译时无法通过约束(类型参数约束)检查的类型参数强制执行运行时检查是一个方便的地方。例如,以下类型使用静态构造函数来强制类型参数是枚举:
1 class Gen<T> where T: struct 2 { 3 static Gen() { 4 if (!typeof(T).IsEnum) { 5 throw new ArgumentException("T must be an enum"); 6 } 7 } 8 }
析构函数
析构函数是一种用于实现销毁一个类的实例所需操作的部件。使用destructor_declaration声明析构函数:
1 destructor_declaration 2 : attributes? 'extern'? '~' identifier '(' ')' destructor_body 3 | destructor_declaration_unsafe 4 ; 5 6 destructor_body 7 : block 8 | ';' 9 ;
destructor_declaration可以包括一组的属性(属性)。
该标识符一个的destructor_declaration必须命名在析构函数声明的类。如果指定了任何其他名称,则会发生编译时错误。
当析构函数声明包含extern
修饰符时,析构函数被称为外部析构函数。因为外部析构函数声明不提供实际实现,所以它的析构函数体由分号组成。对于所有其他析构函数,destructor_body由一个块组成,该块指定要执行的语句以销毁该类的实例。destructor_body精确地相应于method_body实例方法用的void
返回类型(方法体)。
析构函数不是继承的。因此,除了可以在该类中声明的类之外,类没有析构函数。
由于析构函数不需要参数,因此不能重载,因此一个类最多只能有一个析构函数。
析构函数是自动调用的,不能显式调用。当任何代码不再可能使用该实例时,实例就有资格进行销毁。在实例符合销毁条件后,可以在任何时间执行实例的析构函数。当一个实例被破坏时,该实例的继承链中的析构函数按顺序被调用,从大多数派生到最少派生。析构函数可以在任何线程上执行。有关管理析构函数执行时间和方式的规则的进一步讨论,请参阅自动内存管理。
示例的输出
1 using System; 2 3 class A 4 { 5 ~A() { 6 Console.WriteLine("A's destructor"); 7 } 8 } 9 10 class B: A 11 { 12 ~B() { 13 Console.WriteLine("B's destructor"); 14 } 15 } 16 17 class Test 18 { 19 static void Main() { 20 B b = new B(); 21 b = null; 22 GC.Collect(); 23 GC.WaitForPendingFinalizers(); 24 } 25 }
输出是
1 B's destructor 2 A's destructor
因为继承链中的析构函数是按顺序调用的,从大多数派生到最少派生。
析构函数通过重写虚拟方法来实现Finalize
上System.Object
。不允许C#程序覆盖此方法或直接调用它(或覆盖它)。例如,该计划
1 class A 2 { 3 override protected void Finalize() {} // error 4 5 public void F() { 6 this.Finalize(); // error 7 } 8 }
包含两个错误。
编译器的行为就像这个方法及其覆盖根本不存在一样。因此,这个程序:
1 class A 2 { 3 void Finalize() {} // permitted 4 }
是有效的,并且示出的生皮的方法System.Object
的Finalize
方法。
有关从析构函数抛出异常时的行为的讨论,请参阅如何处理异常。
迭代器
使用迭代器块(Blocks)实现的函数成员(Function成员)称为迭代器。
只要相应函数成员的返回类型是枚举器接口(Enumerator接口)或其中一个可枚举接口(Enumerable接口),迭代器块就可以用作函数成员的主体。它可以作为method_body,operator_body或accessor_body发生,而事件,实例构造函数,静态构造函数和析构函数不能作为迭代器实现。
当使用迭代器块实现函数成员时,函数成员的形式参数列表指定任何ref
或out
参数是编译时错误。
枚举器接口
该枚举接口都是非通用接口System.Collections.IEnumerator
和通用接口的所有实例System.Collections.Generic.IEnumerator<T>
。为简洁起见,在本章中,这些接口分别被引用为IEnumerator
和IEnumerator<T>
。
可枚举的接口
该枚举接口都是非通用接口System.Collections.IEnumerable
和通用接口的所有实例System.Collections.Generic.IEnumerable<T>
。为简洁起见,在本章中,这些接口分别被引用为IEnumerable
和IEnumerable<T>
。
收益率类型
迭代器生成一系列值,所有值都相同。此类型称为迭代器的yield类型。
- 产率类型返回一个迭代的
IEnumerator
或IEnumerable
是object
。 - 产率类型返回一个迭代的
IEnumerator<T>
或IEnumerable<T>
是T
。
枚举器对象
当使用迭代器块实现返回枚举器接口类型的函数成员时,调用函数成员不会立即执行迭代器块中的代码。而是创建并返回枚举器对象。此对象封装了迭代器块中指定的代码,并且在MoveNext
调用枚举器对象的方法时,会在迭代器块中执行代码。枚举器对象具有以下特征:
- 它实现
IEnumerator
和IEnumerator<T>
,其中T
是迭代器的产率类型。 - 它实现了
System.IDisposable
。 - 它使用参数值(如果有)的副本和传递给函数成员的实例值进行初始化。
- 它有四种可能的状态,包括之前,运行,暂停和之后,并且最初处于之前的状态。
枚举器对象通常是编译器生成的枚举器类的实例,它将代码封装在迭代器块中并实现枚举器接口,但其他实现方法也是可能的。如果编译器生成了一个枚举器类,那么该类将直接或间接地嵌套在包含该函数成员的类中,它将具有私有可访问性,并且它将保留一个名称供编译器使用(标识符)。
枚举器对象可以实现比上面指定的接口更多的接口。
以下各节描述的确切行为MoveNext
,Current
以及Dispose
成员IEnumerable
和IEnumerable<T>
由枚举器对象提供接口实现。
请注意,枚举器对象不支持该IEnumerator.Reset
方法。调用此方法会导致System.NotSupportedException
抛出a。
MoveNext方法
MoveNext
枚举器对象的方法封装了迭代器块的代码。调用该MoveNext
方法会在迭代器块中执行代码,并根据需要设置Current
枚举器对象的属性。执行的精确操作MoveNext
取决于MoveNext
调用时枚举器对象的状态:
- 如果枚举器对象的状态是之前的,则调用
MoveNext
:- 将状态更改为正在运行。
this
将迭代器块的参数(包括)初始化为初始化枚举器对象时保存的参数值和实例值。- 从头开始执行迭代器块,直到执行中断(如下所述)。
- 如果枚举器对象的状态正在运行,
MoveNext
则未指定调用的结果。 - 如果枚举器对象的状态被挂起,则调用
MoveNext
:- 将状态更改为正在运行。
- 将所有局部变量和参数(包括this)的值恢复为上次暂停执行迭代器块时保存的值。请注意,自上次调用MoveNext以来,这些变量引用的任何对象的内容可能已更改。
- 在
yield return
导致暂停执行的语句之后立即继续执行迭代器块,并继续执行直到执行中断(如下所述)。
- 如果枚举器对象的状态是after,则调用
MoveNext
返回false
。
当MoveNext
执行迭代器块时,可以通过四种方式中断执行:通过yield return
语句,通过yield break
语句,通过遇到迭代器块的结尾,以及抛出异常并将其传播出迭代器块。
- 当一个
yield return
声明中遇到(yield语句):- 计算语句中给出的表达式,隐式转换为yield类型,并将其赋值给
Current
枚举器对象的属性。 - 迭代器主体的执行被暂停。
this
保存所有局部变量和参数(包括)的值,以及此yield return
语句的位置。如果yield return
语句在一个或多个try
块内,finally
则此时不执行关联的块。 - 枚举器对象的状态更改为已挂起。
- 该
MoveNext
方法返回true
其调用者,指示迭代成功前进到下一个值。
- 计算语句中给出的表达式,隐式转换为yield类型,并将其赋值给
- 当一个
yield break
声明中遇到(yield语句):- 如果
yield break
语句在一个或多个try
块内,finally
则执行关联的块。 - 枚举器对象的状态更改为after。
- 该
MoveNext
方法返回false
其调用者,指示迭代完成。
- 如果
- 遇到迭代器体的末尾时:
- 枚举器对象的状态更改为after。
- 该
MoveNext
方法返回false
其调用者,指示迭代完成。
- 抛出异常并从迭代器块传播出来时:
finally
迭代器主体中的适当块将由异常传播执行。- 枚举器对象的状态更改为after。
- 异常传播继续到该
MoveNext
方法的调用者。
当前属性
枚举器对象的Current
属性受yield return
迭代器块中的语句的影响。
当枚举器对象处于挂起状态时,值为Current
前一次调用所设置的值MoveNext
。当枚举器对象处于before,running或after状态时,Current
未指定访问的结果。
对于yield类型不同的迭代器,通过枚举器对象的实现object
访问的结果对应于通过枚举器对象的实现进行访问并将结果转换为。Current
IEnumerable
Current
IEnumerator<T>
object
Dispose方法
该Dispose
方法用于通过将枚举器对象置于after状态来清理迭代。
- 如果枚举器对象的状态是之前
Dispose
的状态,则调用将状态更改为after。 - 如果枚举器对象的状态正在运行,
Dispose
则未指定调用的结果。 - 如果枚举器对象的状态被挂起,则调用
Dispose
:- 将状态更改为正在运行。
- 执行任何finally块,就好像最后执行的
yield return
语句是一个yield break
语句一样。如果这导致抛出异常并将其传播出迭代器主体,则枚举器对象的状态将设置为after,并且异常将传播到Dispose
方法的调用方。 - 将状态更改为after。
- 如果枚举器对象的状态是after,则调用
Dispose
没有任何影响。
可枚举的对象
当使用迭代器块实现返回可枚举接口类型的函数成员时,调用函数成员不会立即执行迭代器块中的代码。而是创建并返回可枚举对象。可枚举对象的GetEnumerator
方法返回一个枚举器对象,该对象封装了迭代器块中指定的代码,并且在MoveNext
调用枚举器对象的方法时,会在迭代器块中执行代码。可枚举对象具有以下特征:
- 它实现
IEnumerable
和IEnumerable<T>
,其中T
是迭代器的产率类型。 - 它使用参数值(如果有)的副本和传递给函数成员的实例值进行初始化。
可枚举对象通常是编译器生成的可枚举类的实例,该类将代码封装在迭代器块中并实现可枚举接口,但是其他实现方法也是可能的。如果编译器生成了一个可枚举的类,那么该类将直接或间接地嵌套在包含该函数成员的类中,它将具有私有可访问性,并且它将保留一个名称供编译器使用(标识符)。
可枚举对象可以实现比上面指定的接口更多的接口。特别地,可枚举对象还可以实现IEnumerator
并IEnumerator<T>
使其能够用作可枚举和枚举器。在该类型的实现中,第一次GetEnumerator
调用可枚举对象的方法时,将返回可枚举对象本身。对可枚举对象的后续调用(GetEnumerator
如果有)将返回可枚举对象的副本。因此,每个返回的枚举器都有自己的状态,一个枚举器中的更改不会影响另一个枚举器。
GetEnumerator方法
可枚举对象提供GetEnumerator
了IEnumerable
和IEnumerable<T>
接口的方法的实现。这两种GetEnumerator
方法共享一个通用实现,它获取并返回一个可用的枚举器对象。枚举器对象被初始化参数值,并且当枚举对象被初始化实例值保存,但否则枚举器对象的功能中描述枚举对象。
实例
本节根据标准C#构造描述了迭代器的可能实现。此处描述的实现基于Microsoft C#编译器使用的相同原则,但它绝不是强制实现或唯一可能的实现。
以下Stack<T>
类GetEnumerator
使用迭代器实现其方法。迭代器按从上到下的顺序枚举堆栈的元素。
1 using System; 2 using System.Collections; 3 using System.Collections.Generic; 4 5 class Stack<T>: IEnumerable<T> 6 { 7 T[] items; 8 int count; 9 10 public void Push(T item) { 11 if (items == null) { 12 items = new T[4]; 13 } 14 else if (items.Length == count) { 15 T[] newItems = new T[count * 2]; 16 Array.Copy(items, 0, newItems, 0, count); 17 items = newItems; 18 } 19 items[count++] = item; 20 } 21 22 public T Pop() { 23 T result = items[--count]; 24 items[count] = default(T); 25 return result; 26 } 27 28 public IEnumerator<T> GetEnumerator() { 29 for (int i = count - 1; i >= 0; --i) yield return items[i]; 30 } 31 }
该GetEnumerator
方法可以转换为编译器生成的枚举器类的实例化,该枚举器类将代码封装在迭代器块中,如下所示。
1 class Stack<T>: IEnumerable<T> 2 { 3 ... 4 5 public IEnumerator<T> GetEnumerator() { 6 return new __Enumerator1(this); 7 } 8 9 class __Enumerator1: IEnumerator<T>, IEnumerator 10 { 11 int __state; 12 T __current; 13 Stack<T> __this; 14 int i; 15 16 public __Enumerator1(Stack<T> __this) { 17 this.__this = __this; 18 } 19 20 public T Current { 21 get { return __current; } 22 } 23 24 object IEnumerator.Current { 25 get { return __current; } 26 } 27 28 public bool MoveNext() { 29 switch (__state) { 30 case 1: goto __state1; 31 case 2: goto __state2; 32 } 33 i = __this.count - 1; 34 __loop: 35 if (i < 0) goto __state2; 36 __current = __this.items[i]; 37 __state = 1; 38 return true; 39 __state1: 40 --i; 41 goto __loop; 42 __state2: 43 __state = 2; 44 return false; 45 } 46 47 public void Dispose() { 48 __state = 2; 49 } 50 51 void IEnumerator.Reset() { 52 throw new NotSupportedException(); 53 } 54 } 55 }
在前面的转换中,迭代器块中的代码被转换为状态机并放置在MoveNext
枚举器类的方法中。此外,局部变量i
将变为枚举器对象中的一个字段,因此它可以在调用的对象中继续存在MoveNext
。
下面的示例打印整数1到10的简单乘法表。FromTo
示例中的方法返回一个可枚举对象,并使用迭代器实现。
1 using System; 2 using System.Collections.Generic; 3 4 class Test 5 { 6 static IEnumerable<int> FromTo(int from, int to) { 7 while (from <= to) yield return from++; 8 } 9 10 static void Main() { 11 IEnumerable<int> e = FromTo(1, 10); 12 foreach (int x in e) { 13 foreach (int y in e) { 14 Console.Write("{0,3} ", x * y); 15 } 16 Console.WriteLine(); 17 } 18 } 19 }
该FromTo
方法可以转换为编译器生成的可枚举类的实例化,该类将代码封装在迭代器块中,如下所示。
1 using System; 2 using System.Threading; 3 using System.Collections; 4 using System.Collections.Generic; 5 6 class Test 7 { 8 ... 9 10 static IEnumerable<int> FromTo(int from, int to) { 11 return new __Enumerable1(from, to); 12 } 13 14 class __Enumerable1: 15 IEnumerable<int>, IEnumerable, 16 IEnumerator<int>, IEnumerator 17 { 18 int __state; 19 int __current; 20 int __from; 21 int from; 22 int to; 23 int i; 24 25 public __Enumerable1(int __from, int to) { 26 this.__from = __from; 27 this.to = to; 28 } 29 30 public IEnumerator<int> GetEnumerator() { 31 __Enumerable1 result = this; 32 if (Interlocked.CompareExchange(ref __state, 1, 0) != 0) { 33 result = new __Enumerable1(__from, to); 34 result.__state = 1; 35 } 36 result.from = result.__from; 37 return result; 38 } 39 40 IEnumerator IEnumerable.GetEnumerator() { 41 return (IEnumerator)GetEnumerator(); 42 } 43 44 public int Current { 45 get { return __current; } 46 } 47 48 object IEnumerator.Current { 49 get { return __current; } 50 } 51 52 public bool MoveNext() { 53 switch (__state) { 54 case 1: 55 if (from > to) goto case 2; 56 __current = from++; 57 __state = 1; 58 return true; 59 case 2: 60 __state = 2; 61 return false; 62 default: 63 throw new InvalidOperationException(); 64 } 65 } 66 67 public void Dispose() { 68 __state = 2; 69 } 70 71 void IEnumerator.Reset() { 72 throw new NotSupportedException(); 73 } 74 } 75 }
可枚举类实现了可枚举接口和枚举器接口,使其既可以作为枚举也可以作为枚举器。第一次GetEnumerator
调用该方法时,将返回可枚举对象本身。对可枚举对象的后续调用(GetEnumerator
如果有)将返回可枚举对象的副本。因此,每个返回的枚举器都有自己的状态,一个枚举器中的更改不会影响另一个枚举器。该Interlocked.CompareExchange
方法用于确保线程安全操作。
在from
和to
参数都变成了在枚举类的字段。因为from
在迭代器块中进行了修改,所以__from
引入了一个附加字段来保存from
每个枚举器中给出的初始值。
该MoveNext
方法抛出InvalidOperationException
,如果当它被称为__state
是0
。这样可以防止在不首先调用的情况下将可枚举对象用作枚举器对象GetEnumerator
。
以下示例显示了一个简单的树类。的Tree<T>
类实现其GetEnumerator
使用迭代方法。迭代器以中缀顺序枚举树的元素。
1 using System; 2 using System.Collections.Generic; 3 4 class Tree<T>: IEnumerable<T> 5 { 6 T value; 7 Tree<T> left; 8 Tree<T> right; 9 10 public Tree(T value, Tree<T> left, Tree<T> right) { 11 this.value = value; 12 this.left = left; 13 this.right = right; 14 } 15 16 public IEnumerator<T> GetEnumerator() { 17 if (left != null) foreach (T x in left) yield x; 18 yield value; 19 if (right != null) foreach (T x in right) yield x; 20 } 21 } 22 23 class Program 24 { 25 static Tree<T> MakeTree<T>(T[] items, int left, int right) { 26 if (left > right) return null; 27 int i = (left + right) / 2; 28 return new Tree<T>(items[i], 29 MakeTree(items, left, i - 1), 30 MakeTree(items, i + 1, right)); 31 } 32 33 static Tree<T> MakeTree<T>(params T[] items) { 34 return MakeTree(items, 0, items.Length - 1); 35 } 36 37 // The output of the program is: 38 // 1 2 3 4 5 6 7 8 9 39 // Mon Tue Wed Thu Fri Sat Sun 40 41 static void Main() { 42 Tree<int> ints = MakeTree(1, 2, 3, 4, 5, 6, 7, 8, 9); 43 foreach (int i in ints) Console.Write("{0} ", i); 44 Console.WriteLine(); 45 46 Tree<string> strings = MakeTree( 47 "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"); 48 foreach (string s in strings) Console.Write("{0} ", s); 49 Console.WriteLine(); 50 } 51 }
该GetEnumerator
方法可以转换为编译器生成的枚举器类的实例化,该枚举器类将代码封装在迭代器块中,如下所示。
1 class Tree<T>: IEnumerable<T> 2 { 3 ... 4 5 public IEnumerator<T> GetEnumerator() { 6 return new __Enumerator1(this); 7 } 8 9 class __Enumerator1 : IEnumerator<T>, IEnumerator 10 { 11 Node<T> __this; 12 IEnumerator<T> __left, __right; 13 int __state; 14 T __current; 15 16 public __Enumerator1(Node<T> __this) { 17 this.__this = __this; 18 } 19 20 public T Current { 21 get { return __current; } 22 } 23 24 object IEnumerator.Current { 25 get { return __current; } 26 } 27 28 public bool MoveNext() { 29 try { 30 switch (__state) { 31 32 case 0: 33 __state = -1; 34 if (__this.left == null) goto __yield_value; 35 __left = __this.left.GetEnumerator(); 36 goto case 1; 37 38 case 1: 39 __state = -2; 40 if (!__left.MoveNext()) goto __left_dispose; 41 __current = __left.Current; 42 __state = 1; 43 return true; 44 45 __left_dispose: 46 __state = -1; 47 __left.Dispose(); 48 49 __yield_value: 50 __current = __this.value; 51 __state = 2; 52 return true; 53 54 case 2: 55 __state = -1; 56 if (__this.right == null) goto __end; 57 __right = __this.right.GetEnumerator(); 58 goto case 3; 59 60 case 3: 61 __state = -3; 62 if (!__right.MoveNext()) goto __right_dispose; 63 __current = __right.Current; 64 __state = 3; 65 return true; 66 67 __right_dispose: 68 __state = -1; 69 __right.Dispose(); 70 71 __end: 72 __state = 4; 73 break; 74 75 } 76 } 77 finally { 78 if (__state < 0) Dispose(); 79 } 80 return false; 81 } 82 83 public void Dispose() { 84 try { 85 switch (__state) { 86 87 case 1: 88 case -2: 89 __left.Dispose(); 90 break; 91 92 case 3: 93 case -3: 94 __right.Dispose(); 95 break; 96 97 } 98 } 99 finally { 100 __state = 4; 101 } 102 } 103 104 void IEnumerator.Reset() { 105 throw new NotSupportedException(); 106 } 107 } 108 }
编译器生成的foreach
语句中使用的临时数被提升到枚举器对象的__left
和__right
字段中。__state
仔细更新枚举器对象的字段,以便在Dispose()
抛出异常时正确调用正确的方法。请注意,无法使用简单foreach
语句编写已翻译的代码。
异步功能
使用修饰符的方法(方法)或匿名函数(匿名函数表达式)async
称为异步函数。通常,术语async用于描述具有async
修饰符的任何类型的函数。
async函数的形式参数列表指定任何ref
或out
参数是编译时错误。
所述return_type异步方法的必须是void
或任务类型。任务类型是System.Threading.Tasks.Task
和构造的类型System.Threading.Tasks.Task<T>
。为简洁起见,在本章中,这些类型分别作为Task
和引用Task<T>
。返回任务类型的异步方法被称为任务返回。
任务类型的确切定义是实现定义的,但从语言的角度来看,任务类型处于不完整,成功或出现故障的状态之一。出现故障的任务会记录相关的异常。成功Task<T>
记录类型的结果T
。任务类型是等待的,因此可以是await表达式的操作数(Await表达式)。
异步函数调用能够通过其正文中的await表达式(Await表达式)暂停评估。稍后可以通过恢复代表在暂停等待表达时恢复评估。恢复委托是类型的System.Action
,当它被调用时,异步函数调用的评估将从它停止的await表达式恢复。如果函数调用从未被挂起,则异步函数调用的当前调用者是原始调用者,否则是最近调用者的调用者。
评估任务返回异步功能
调用任务返回异步函数会导致生成返回任务类型的实例。这称为异步函数的返回任务。该任务最初处于不完整状态。
然后评估异步函数体,直到它被挂起(通过到达await表达式)或终止,此时控制权与返回任务一起返回给调用者。
当异步函数的主体终止时,返回任务将移出不完整状态:
- 如果函数体因到达return语句或正文结尾而终止,则任何结果值都将记录在返回任务中,该任务将进入成功状态。
- 如果函数体因未捕获异常(throw语句)而终止,则异常将记录在返回任务中,该任务将进入故障状态。
评估返回空隙的异步函数
如果异步函数的返回类型是void
,则评估与以上方式的不同之处在于:由于没有返回任务,该函数会将当前线程的同步上下文的完成和异常通知。同步上下文的确切定义是依赖于实现的,但是表示当前线程正在运行的“where”。当评估void返回异步函数开始,成功完成或引发未捕获的异常时,将通知同步上下文。
这允许上下文跟踪在其下运行的返回异步函数的数量,并决定如何传播来自它们的异常。