C# 语言规范_版本5.0 (第10章 类)
1. 类
类是一种数据结构,它可以包含数据成员(常量和字段)、函数成员(方法、属性、事件、索引器、运算符、实例构造函数、静态构造函数和析构函数)以及嵌套类型。类类型支持继承,继承是一种机制,它使派生类可以对基类进行扩展和专用化。
1.1 类声明
class-declaration 是一个 type-declaration(第 9.6 节),它用于声明一个新类。
class-declaration:
attributesopt
class-modifiersopt partialopt class identifier type-parameter-listopt
class-baseopt type-parameter-constraints-clausesopt class-body
;opt
A class-declaration 的组成结构如下:开头是一组可选 attributes(第 17 章),然后依次是一组可选 class-modifiers(第 10.1.1 节)、可选 partial 修饰符、关键字 class 和用于命名类的 identifier、可选 type-parameter-list (第 10.1.3 节)、可选 class-base 规范(第 10.1.4 节)、一组可选 type-parameter-constraints-clauses (第 10.1.5 节)、class-body(第 10.1.6 节),最后是一个分号(可选)。
只有提供了 type-parameter-list 后,类声明才可以提供 type-parameter-constraints-clauses。
type-parameter-list 的类声明是一个泛型类声明 (generic class declaration)。此外,任何嵌套在泛型类声明或泛型结构声明中的类本身就是一个泛型类声明,因为必须为包含类型提供类型形参才能创建构造类型。
1.1.1 类修饰符
class-declaration 可以根据需要包含一个类修饰符序列:
class-modifiers:
class-modifier
class-modifiers class-modifier
class-modifier:
new
public
protected
internal
private
abstract
sealed
static
同一修饰符在一个类声明中多次出现是编译时错误。
new 修饰符适用于嵌套类。它指定类隐藏同名的继承成员,详见第 10.3.4 节中的介绍。如果在不是嵌套类声明的类声明中使用 new 修饰符,则会导致编译时错误。
public、protected、internal 和 private 修饰符将控制类的可访问性。根据类声明所处的上下文,这其中的一些修饰符可能不允许使用(第 3.5.1 节)。
在以下几节中对 abstract、sealed 和 static 修饰符进行了讨论。
1.1.1.1 抽象类
abstract 修饰符用于表示所修饰的类是不完整的,并且它只能用作基类。抽象类与非抽象类在以下方面是不同的:
- 抽象类不能直接实例化,并且对抽象类使用 new 运算符会导致编译时错误。虽然一些变量和值在编译时的类型可以是抽象的,但是这样的变量和值必须或者为 null,或者含有对非抽象类的实例的引用(此非抽象类是从抽象类型派生的)。
- 允许(但不要求)抽象类包含抽象成员。
- 抽象类不能被密封。
当从抽象类派生非抽象类时,这些非抽象类必须具体实现所继承的所有抽象成员,从而重写那些抽象成员。在下面的示例中
abstract
class A
{
public abstract void F();
}
abstract
class B: A
{
public void G() {}
}
class C: B
{
public override void F() {
// actual implementation of F
}
}
抽象类 A 引入抽象方法 F。类 B 引入另一个方法 G,但由于它不提供 F 的实现,B 也必须声明为抽象类。类 C 重写 F,并提供一个具体实现。由于 C 没有抽象成员,因此 C 可以(但不要求)是非抽象的。
1.1.1.2 密封类
sealed 修饰符用于防止从所修饰的类派生出其他类。如果一个密封类被指定为其他类的基类,则会发生编译时错误。
密封类不能同时为抽象类。
sealed 修饰符主要用于防止非有意的派生,但是它还能促使某些运行时优化。具体而言,由于密封类永远不会有任何派生类,所以对密封类的实例的虚函数成员的调用可以转换为非虚调用来处理。
1.1.1.3 静态类
static 修饰符用于标记声明为静态类 (static class) 的类。静态类不能实例化,不能用作类型,而且仅可以包含静态成员。只有静态类才能包含扩展方法的声明(第 10.6.9 节)。
静态类声明受以下限制:
- 静态类不能包含 sealed 或 abstract 修饰符。但是,注意,因为无法实例化静态类或从静态类派生,所以静态类的行为就好像既是密封的又是抽象的。
- 静态类不能包含 class-base 规范(第 10.1.4 页),不能显式指定基类或所实现接口的列表。静态类隐式从 object 类型继承。
- 静态类只能包含静态成员(第 10.3.7 节)。注意,常量和嵌套类型归为静态成员。
- 静态类不能含有声明的可访问性为 protected 或 protected internal 的成员。
违反上述任何限制都将导致编译时错误。
静态类没有实例构造函数。静态类中不能声明实例构造函数,并且对于静态类也不提供任何默认实例构造函数(第 10.11.4 节)。
静态类的成员并不会自动成为静态的,成员声明中必须显式包含一个 static 修饰符(常量和嵌套类型除外)。当一个类嵌套在一个静态的外层类中时,除非该类显式包含 static 修饰符,否则该嵌套类不是静态类。
1.1.1.3.1 引用静态类类型
如果下列条件成立,则允许 namespace-or-type-name(第 3.8 节)引用静态类:
- namespace-or-type-name 是 T.I 形式的 namespace-or-type-name 中的 T,或者
- namespace-or-type-name 是 typeof(T) 形式的 typeof-expression(第 7.5.11 节)中的 T。
如果下列条件成立,则允许 primary-expression(第 7.5 节)引用静态类:
- primary-expression 为 E.I 形式的 member-access(第 7.5.4 节)中的 E。
在任何其他上下文中,引用静态类将导致编译时错误。例如,将静态类用作基类、成员的构成类型(第 10.3.8 节)、泛型类型实参或类型形参约束都是错误的。同样,静态类不可用于数组类型、指针类型、new 表达式、强制转换表达式、is 表达式、as 表达式、sizeof 表达式或默认值表达式。
1.1.2 分部修饰符
partial 修饰符用于指示此 class-declaration 是分部类型声明。包容命名空间或类型声明中的多个同名分部类型声明按照第 10.2 节中指定的规则组合成一个类型声明。
如果程序文本的各独立段是在不同的上下文中产生或维护的,则在这些段上分布类声明非常有用。例如,类声明的某一部分可能是计算机生成的,而另一部分可能是手动创作的。将这两部分文本分开可以防止某人所做的更新与他人所做的更新发生冲突。
1.1.3 类型形参
类型形参是一个简单标识符,代表一个为创建构造类型而提供的类型实参的占位符。类型形参是将来提供的类型的形式占位符。而类型实参(第 4.4.1 节)是在创建构造类型时替换类型形参的实际类型。
type-parameter-list:
< type-parameters >
type-parameters:
attributesopt type-parameter
type-parameters , attributesopt type-parameter
type-parameter:
identifier
类声明中的每个类型形参在该类的声明空间(第 3.3 节)中定义一个名称。因此,它不能与另一个类型形参或该类中声明的成员具有相同的名称。类型形参不能与类型本身具有相同的名称。
1.1.4 类基本规范
类声明可以包含 class-base 规范,它定义该类的直接基类和由该类直接实现的接口(第 13 章)。
class-base:
:
class-type
:
interface-type-list
:
class-type , interface-type-list
interface-type-list:
interface-type
interface-type-list , interface-type
类声明中指定的基类可以是构造类类型(第 4.4 节)。基类本身不能是类型形参,但在其作用域中可以包含类型形参。
class Extend<V>: V {} // Error, type parameter used as base class
1.1.4.1 基类
当 class-type 中包含一个 class-base 时,它表示该类就是所声明的类的直接基类。如果一个类声明中没有 class-base,或 class-base 只列出接口类型,则假定直接基类就是 object。一个类会从它的直接基类继承成员,如第 10.3.3 节中所述。
在下面的示例中
class A {}
class B: A {}
称类 AA 为类 B 的直接基类,而称 B 是从 A 派生的。由于 A 没有显式地指定直接基类,它的直接基类隐含地为 object。
对于构造类类型,如果泛型类声明中指定了基类,则通过将基类声明中的每个 type-parameter 替换为构造类型的对应 type-argument 来获得构造类型的基类。假设有下面的泛型类声明
class B<U,V> {...}
class G<T>: B<string,T[]> {...}
构造类型 G<int> 的基类将是 B<string,int[]>。
类类型的直接基类必须至少与类类型(第 3.5.2 节)本身具有同样的可访问性。例如,试图从 private 或 internal 类派生一个 public 类,会导致编译时错误。
类类型的直接基类不能为下列任一类型:System.Array、System.Delegate、System.MulticastDelegate、System.Enum 或 System.ValueType。另外,泛型类声明不能将 System.Attribute 用作直接或间接基类。
在确定类 B 的直接基类规范 A 的含义时,将 B 的直接基类临时假定为 object。这在直观上确保基类规范的含义无法递归依赖其自身。示例:
class A<T> {
public class B{}
}
class C : A<C.B> {}
是错误的,因为在基类规范 A<C.B> 中,将 C 的直接基类视为 object,因此(根据第 3.8 节中的规则)不将 C 视为具有成员 B。
一个类类型的基类包括它的直接基类以及该直接基类的基类。换句话说,基类集是直接基类关系的传递闭包。在上面的示例中,B 的基类是 A 和 object。在下面的示例中
class A {...}
class B<T>: A {...}
class C<T>: B<IComparable<T>> {...}
class D<T>: C<T[]> {...}
D<int> 的基类为 C<int[]>、B<IComparable<int[]>>、A 和 object。
除了类 object,每个类类型都只有一个直接基类。object 类没有任何直接基类,并且是所有其他类的终极基类。
当类 B 从类 A 派生时,A 依赖于 B 会导致编译时错误。类直接依赖于 (directly depends on) 它的直接基类(如果有),并且还直接依赖于它直接嵌套于其中的类(如果有)。从上述定义可以推出:一个类所依赖的类的完备集就是此直接依赖于(directly depends on) 关系的自反和传递闭包。
下面的示例
class A: A {}
是错误的,因为该类依赖于其自身。同样,示例
class A: B {}
class B: C {}
class C: A {}
是错误的,因为这些类之间循环依赖。最终,示例
class A: B.C {}
class B: A
{
public class C {}
}
也会导致编译时错误,原因是 A 依赖于 B.C(它的直接基类),B.C 依赖于 B(它的直接包容类),而 B 又循环地依赖于 A。
请注意,一个类不依赖于嵌套在它内部的类。在下面的示例中
class A
{
class B: A {}
}
B 依赖于 A(原因是 A 既是它的直接基类又是它的直接包容类),但是 A 不依赖于 B(因为 B 既不是 A 的基类也不是它的包容类)。因此,此示例是有效的。
不能从一个 sealed 类派生出别的类。在下面的示例中
sealed class A {}
class B: A {} // Error, cannot derive from a sealed class
类 B 是错误的,因为它试图从 sealed 类 A 中派生。
1.1.4.2 接口实现
class-base 规范中可以包含一个接口类型列表,这表示所声明的类直接实现所列出的各个接口类型。第 13.4 节对接口实现进行了进一步讨论。
1.1.5 类型形参约束
泛型类型和方法声明可以选择通过包括 type-parameter-constraints-clause 来指定类型形参约束。
type-parameter-constraints-clauses:
type-parameter-constraints-clause
type-parameter-constraints-clauses
type-parameter-constraints-clause
type-parameter-constraints-clause:
where type-parameter : type-parameter-constraints
type-parameter-constraints:
primary-constraint
secondary-constraints
constructor-constraint
primary-constraint , secondary-constraints
primary-constraint , constructor-constraint
secondary-constraints , constructor-constraint
primary-constraint , secondary-constraints , constructor-constraint
primary-constraint:
class-type
class
struct
secondary-constraints:
interface-type
type-parameter
secondary-constraints , interface-type
secondary-constraints , type-parameter
constructor-constraint:
new ( )
每个 type-parameter-constraints-clause 都包括标记 where,后面跟着类型形参的名称,再后面则跟着一个冒号和该类型形参的约束列表。每个类型形参最多只能有一个 where 子句,并且 where 子句可以按任何顺序列出。与属性访问器中的 get 和 set 标记一样,where 标记也不是关键字。
where 子句中给出的约束列表可以包括以下任一组件,依次为:一个主要约束、一个或多个次要约束 \b 以及构造函数约束 \b \b new()。
主要约束可以是类类型、引用类型约束 (reference type constraint) class,也可以是值类型约束 (value type constraint) struct。次要约束可以是 type-parameter,也可以是 interface-type。
引用类型约束指定用于类型形参的类型实参必须是引用类型。所有类类型、接口类型、委托类型、数组类型和已知将是引用类型(将在下面定义)的类型形参都满足此约束。
值类型约束指定用于类型形参的类型实参必须是不可以为 null 值的类型。所有不可以为 null 的结构类型、枚举类型和具有值类型约束的类型形参都满足此约束。注意,虽然可以为 null 的类型(第 4.1.10 节)被归为值类型,但是不满足值类型约束。具有值类型约束的类型形参还不能具有 constructor-constraint。
指针类型从不允许作为类型实参,并且不被视为满足引用类型或值类型约束。
如果约束是类类型、接口类型或类型形参,则该类型指定用于该类型形参的每个类型实参必须支持的最低“基类型”。每当使用构造类型或泛型方法时,都会在编译时根据类型形参上的约束检查类型实参。所提供的类型实参必须满足第 4.4.4 节中给出的条件。
class-type 约束必须满足下列规则:
- 该类型必须是类类型。
- 该类型一定不能是 sealed。
- 该类型不能是以下类型之一:System.Array、System.Delegate、System.Enum 或 System.ValueType。
- 该类型一定不能是 object。由于所有类型都派生自 object,允许这样的约束没有任何作用。
- 给定的类型形参至多只能有一个约束可以是类类型。
指定为 interface-type 约束的类型必须满足下列规则:
- 该类型必须是接口类型。
- 不能在给定的 where 子句中多次指定某个类型。
在任一情况下,该约束都可以包括关联的类型或方法声明的任何类型形参作为构造类型的组成部分,并且可以包括被声明的类型。
指定为类型形参约束的任何类或接口类型必须至少与声明的泛型类型或方法具有相同的可访问性(第 3.5.4 节)。
指定为 type-parameter 约束的类型必须满足下列规则:
- 该类型必须是类型形参。
- 不能在给定的 where 子句中多次指定某个类型。
此外,类型形参的依赖关系图中一定不能存在循环,其中依赖性是通过下列方式定义的传递关系:
- 如果类型形参 T 用作类型形参 S 的约束,则 S 依赖 (depend on) T。
- 如果类型形参 S 依赖类型形参 T,并且 T 依赖类型形参 U,则 S 依赖 U。
根据这个关系,如果类型形参依赖自身(直接或间接),则会产生编译时错误。
相互依赖的类型形参之间的任何约束都必须一致。如果类型形参 S 依赖类型形参 T,则:
- T 一定不能具有值类型约束。否则,T 被有效地密封,使得 S 将被强制为与 T 相同的类型,从而消除了使用这两个类型形参的需要。
- 如果 S 具有值类型约束,则 T 不能具有 class-type 约束。
- 如果 S 具有 class-type 约束 A,T 具有 class-type 约束 B,则必须存在从 A 到 B 的标识转换或隐式引用转换或者从 B 到 A 的隐式引用转换。
- 如果 S 还依赖类型形参 U,并且 U 具有 class-type 约束 A,T 具有 class-type 约束 B,则必须存在从 A 到 B 的标识转换或隐式引用转换或者从 B 到 A 的隐式引用转换。
S 具有值类型约束而 T 具有引用类型约束是有效的。这实际上将 T 限制为类型 System.Object、System.ValueType、System.Enum 和任何接口类型。
如果类型形参的 where 子句包括构造函数约束(具有 new() 形式),则可以使用 new 运算符创建该类型的实例(第 7.6.10.1 节)。用于具有构造函数约束的类型形参的任何类型实参都必须具有公共的无形参构造函数(任何值类型都隐式地存在此构造函数),或者是具有值类型约束或构造函数约束的类型形参(有关详细信息,请参见第 10.1.5 节)。
下面是约束的示例:
interface IPrintable
{
void Print();
}
interface IComparable<T>
{
int CompareTo(T value);
}
interface IKeyProvider<T>
{
T
GetKey();
}
class Printer<T> where T: IPrintable {...}
class SortedList<T> where T: IComparable<T> {...}
class Dictionary<K,V>
where K: IComparable<K>
where V: IPrintable,
IKeyProvider<K>, new()
{
...
}
下面的示例是错误的,因为它将导致类型形参的依赖关系发生循环:
class Circular<S,T>
where S: T
where T: S // Error, circularity in dependency graph
{
...
}
下面的示例演示其他无效情况:
class Sealed<S,T>
where S: T
where T: struct // Error, T is sealed
{
...
}
class A {...}
class B {...}
class Incompat<S,T>
where S: A, T
where T: B // Error, incompatible class-type constraints
{
...
}
class StructWithClass<S,T,U>
where S: struct, T
where T: U
where U: A // Error, A incompatible with struct
{
...
}
类型 C 的动态抹除 为类型 Co,构造如下:
- 如果 C 为嵌套类型 Outer.Inner,则 Co 为嵌套类型 Outero.Innero。
- 如果 C 为构造类型 G<A1、… 和 An> 以及类型实参 A1、… 和 An,则 Co 为构造类型 G<A1o、… 和 Ano>。
- 如果 C 为数组类型 E[],则 Co 为数组类型 Eo[]。
- 如果 C 为 指针类型 E*,则 Co 为指针类型 Eo*。
- 如果 C 为 dynamic,则 Co 为 object.
- 否则,Co 为 C。
类型形参 T 的有效基类 (effective base class) 定义如下:
让 R 成为类型集以便:
- 对于属于 type-parameter 的 T的每个约束,R 包含其有效基类。
- 对于属于 struct-type 的 T 的每个约束,R 包含 System.ValueType。
- 对于属于 enumeration type 的 T 的每个约束,R 包含 System.Enum。
- 对于属于 delegate type 的 T 的每个约束,R 包含其动态抹除。
- 对于属于 array type 的 T 的每个约束,R 包含 System.Array。
- 对于属于 class-type 的 T 的每个约束,R 包含其动态抹除。
则
- 如果 T 具有值类型约束,则其有效基类为 System.ValueType。
- 否则,如果 R 为空,则有效基类为 object。
- 否则,T 的有效基类是集 R 的被包含程度最大的类型(第 6.4.3 节)。如果该集没有被包含的类型,则 T 的有效基类为 object。一致性规则确保存在被包含程度最大的类型。
如果类型形参是其约束继承自基类方法的方法类型形参,则将在类型替换后计算有效基类。
这些规则可确保有效基类始终为 class-type。
类型形参 T 的有效接口集 (effective interface set) 定义如下:
- 如果 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,则该类型形参将视为一个引用类型 (known to be a reference type)。
受约束的类型形参类型的值可用于访问约束所暗示的实例成员。在下面的示例中
interface IPrintable
{
void Print();
}
class Printer<T> where T: IPrintable
{
void PrintOne(T x) {
x.Print();
}
}
可直接对 x 调用 IPrintable 的方法,因为 T 被约束为始终实现 IPrintable。
1.1.6 类体
一个类的 class-body 用于定义该类的成员。
class-body:
{ class-member-declarationsopt }
1.2 分部类型
类型声明可以分为多个分部类型声明 (partial type declaration)。类型声明由它的各部分按照本节中的规则进行构造,因此在程序编译时和运行时的其余处理过程中,类型声明按单个声明处理。
如果 class-declaration、 struct-declaration 或 interface-declaration 包含 partial 修饰符,则它表示分部类型声明。partial 不是关键字,仅在它紧靠关键字 class、struct 或 interface 中的某一个之前出现在类型声明中或紧靠类型 void 之前出现在方法声明中时充当修饰符。在其他上下文中,它可用作正常标识符。
分部类型声明中的每一部分都必须包括一个 partial 修饰符。它必须和其他部分具有相同名称,并且必须与其他部分在同一命名空间或类型声明中声明。partial 修饰符的出现指示其他位置可能还有类型声明的其他部分,但是这些其他部分并非必须存在;对于只具有一个声明的类型,包含 partial 修饰符也是有效的。
分部类型的所有部分必须一起编译,以便这些部分可在编译时合并为单个类型声明。特别指出的是,分部类型不允许对已经编译的类型进行扩展。
可使用 partial 修饰符在多个部分中声明嵌套类型。通常,其包含类型也使用 partial 声明,并且嵌套类型的每个部分均在该包含类型的不同部分中声明。
不允许使用 partial 修饰符声明委托或枚举。
1.2.1 特性
分部类型的特性是通过组合每个部分的特性(不指定顺序)来确定的。如果对多个部分放置同一个特性,则相当于多次对该类型指定此特性。例如,下面的两个部分:
[Attr1, Attr2("hello")]
partial class A {}
[Attr3, Attr2("goodbye")]
partial class A {}
相当于下面的声明:
[Attr1, Attr2("hello"), Attr3,
Attr2("goodbye")]
class A {}
类型形参的特性以类似的方式进行组合。
1.2.2 修饰符
当分部类型声明指定了可访问性(public、protected、internal 和 private 修饰符)时,它必须与所有其他指定了可访问性的部分一致。如果分部类型的所有部分都未指定可访问性,则会向该分部类型提供相应的默认可访问性(第 3.5.1 节)。
如果嵌套类型的一个或多个分部声明包含 new 修饰符,则在嵌套类型对继承的成员进行了隐藏(第 3.7.1.2 节)的情况不会报告任何警告。
如果某个类的一个或多个分部声明包含 abstract 修饰符,则该类被视为抽象类(第 10.1.1.1 节)。否则,该类被视为非抽象类。
如果某个类的一个或多个分部声明包含 sealed 修饰符,则该类被视为密封类(第 10.1.1.2 节)。否则,该类被视为非封闭类。
注意,一个类不能既是抽象类又是密封类。
当 unsafe 修饰符用于某个分部类型声明时,只有该特定部分才被视为不安全的上下文(第 18.1 节)。
1.2.3 类型形参和约束
如果在多个部分中声明泛型类型,则每个部分都必须声明类型形参。每个部分都必须有相同数目的类型形参,并且每个类型形参按照顺序有相同的名称。
当分部泛型类型声明包含约束(where 子句)时,该约束必须与包含约束的所有其他部分一致。具体而言,包含约束的每个部分都必须有针对相同的类型形参集的约束,并且对于每个类型形参,主要、次要和构造函数约束集都必须等效。如果两个约束集包含相同的成员,则它们等效。如果某个分部泛型类型的任何部分都未指定类型形参约束,则该类型形参被视为无约束。
下面的示例
partial class Dictionary<K,V>
where K: IComparable<K>
where V: IKeyProvider<K>,
IPersistable
{
...
}
partial class Dictionary<K,V>
where V: IPersistable,
IKeyProvider<K>
where K: IComparable<K>
{
...
}
partial class Dictionary<K,V>
{
...
}
是正确的,因为包含约束的那些部分(前两个)实际上分别对相同的类型形参集指定了相同的主要、次要和构造函数约束集。
1.2.4 基类
当一个分部类声明包含基类说明时,它必须与包含基类说明的所有其他部分一致。如果某个分部类的任何部分都不包含基类说明,则基类将为 System.Object(第 10.1.4.1 节)。
1.2.5 基接口
在多个部分中声明的类型的基接口集是每个部分中指定的基接口的并集。一个特定基接口在每个部分中只能指定一次,但是允许多个部分指定相同的基接口。任何给定基接口的成员只能有一个实现。
在下面的示例中
partial class C: IA, IB {...}
partial class C: IC {...}
partial class C: IA, IB {...}
类 C 的基接口集为 IA、IB 和 IC。
通常,每个部分都提供了在该部分上声明的接口的实现;但这不是必需的。一个部分可能提供在另一个部分上声明的接口的实现:
partial class X
{
int IComparable.CompareTo(object o) {...}
}
partial class X: IComparable
{
...
}
1.2.6 成员
除了分部方法(第 10.2.7 节),在多个部分中声明的类型的成员集仅仅是在每个部分中声明的成员集的并集。所有部分的类型声明主体共享相同的声明空间(第 3.3 节),并且每个成员的范围(第 3.7 节)都扩展到所有部分的主体。任何成员的可访问性域总是包含包容类型的所有部分;在一个部分中声明的 private 成员可从其他部分随意访问。在类型的多个部分中声明同一个成员将引起编译时错误,除非该成员是带有 partial 修饰符的类型。
partial class A
{
int x; //
Error, cannot declare x more than once
partial
class Inner // Ok, Inner is a partial
type
{
int y;
}
}
partial class A
{
int x; //
Error, cannot declare x more than once
partial
class Inner // Ok, Inner is a partial type
{
int z;
}
}
类型中的成员的顺序对于 C# 代码无关紧要,但是在与其他语言或环境交互时,这可能很重要。在这些情况下,没有对分为多个部分声明的类型内的成员顺序进行定义。
1.2.7 分部方法
分部方法可以在类型声明的一个部分中定义,而在另一个部分中实现。实现是可选的;如果所有部分都未实现分部方法,则将从组合各部分而构成的类型声明中,移除分部方法声明和所有对它的调用。
分部方法不能定义访问修饰符,而隐含为 private。它们的返回类型必须是 void,而且它们的形参不能带有 out 修饰符。在方法声明中,仅当标识符 partial 紧靠 void 类型之前出现时,才将它识别为特殊关键字,否则,将它用作正常标识符。分部方法不能显式实现接口方法。
有两种类型的分部方法声明:如果方法声明体是一个分号,则称该声明是定义分部方法声明 (defining partial method declaration)。如果以 block 形式给定该声明体,则称该声明是实现分部方法声明 (implementing partial method declaration)。在类型声明的各个部分,只能有一个具有给定签名的定义分部方法声明,也只能有一个具有给定签名的实现分部方法声明。如果给定了实现分部方法声明,则必须存在相应的定义分部方法声明,并且这两个声明必须符合以下指定的内容:
- 这两个声明必须具有相同的修饰符(但不必采用同一顺序)、方法名、类型形参数目和形参数目。
- 声明中相应的形参必须具有相同的修饰符(但不必采用同一顺序)和相同的类型(类型形参名称中的模不同)。
- 声明中的相应类型形参必须具有相同的约束(类型形参名称中的模不同)。
实现分部方法声明可以与相应的定义分部方法声明出现在同一部分。
只有定义分部方法会参与重载决策。因此,无论是否给定实现声明,调用表达式都可以解析分部方法的调用。因为分部方法始终返回 void,所以此类调用表达式始终为表达式语句。而且,因为分部方法隐含为 private,所以此类语句将始终在声明了该分部方法的类型声明的其中某一部分出现。
如果分部类型声明中的任何部分都不包含给定分部方法的实现声明,则调用它的任何表达式语句都将仅从组合的类型声明中移除。因此,调用表达式(包括所有构成表达式)在运行时将不起作用。分部方法本身也将从组合的类型声明中移除,不再是其中的成员。
如果给定的分部方法存在实现声明,则分部方法的调用将保留。分部方法将产生类似于实现分部方法声明的方法声明,但以下内容除外:
- 不包括 partial 修饰符
- 结果方法声明中的特性是未指定顺序的定义分部方法声明和实现分部方法声明的组合特性。不移除重复项。
- 结果方法声明中的形参的特性是未指定顺序的定义分部方法声明和实现分部方法声明的相应形参的组合特性。不移除重复项。
如果为分部方法 M 指定的是定义声明而不是实现声明,则应用以下限制:
- 如果创建该方法的委托(第 7.6.10.5 节),则会出现编译时错误。
- 如果在转换为表达式目录树类型(第 6.5.2 节)的匿名函数内引用 M,则会出现编译时错误。
- 作为调用 M 的一部分出现的表达式不影响明确赋值状态(第 5.3 节),这可能会导致编译时错误。
- M 不能是应用程序的入口点(第 3.1 节)。
分部方法对于允许类型声明的一部分自定义另一部分的行为(例如由工具生成的行为)非常有用。请考虑以下分部类声明:
partial class Customer
{
string name;
public string Name {
get { return name; }
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
partial void OnNameChanging(string newName);
partial
void OnNameChanged();
}
如果该类不与其他任何部分一起编译,则将移除定义分部方法声明及其调用,产生的组合类声明将等效于以下内容:
class Customer
{
string name;
public string Name {
get { return name; }
set
{ name = value; }
}
}
但是,假设给定的是另一部分,该部分提供分部方法的实现声明:
partial class Customer
{
partial void OnNameChanging(string
newName)
{
Console.WriteLine(“Changing “ + name
+ “ to “ + newName);
}
partial
void OnNameChanged()
{
Console.WriteLine(“Changed to “ + name);
}
}
那么,产生的组合类声明将等效于以下内容:
class Customer
{
string name;
public string Name {
get { return name; }
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
void
OnNameChanging(string newName)
{
Console.WriteLine(“Changing “ + name
+ “ to “ + newName);
}
void
OnNameChanged()
{
Console.WriteLine(“Changed to “ + name);
}
}
1.2.8 名称绑定
虽然可扩展类型的每个部分都必须在同一命名空间中声明,但是这些部分通常在不同的命名空间声明下编写。因此,每个部分可能存在不同的 using 指令(第 9.4 节)。当解释一个部分内的简单名称(第 7.5.2 节)时,只考虑包容该部分的命名空间定义的 using 指令。这可能会导致同一标识符在不同部分中具有不同的含义:
namespace N
{
using List = System.Collections.ArrayList;
partial
class A
{
List x; // x has type System.Collections.ArrayList
}
}
namespace N
{
using List = Widgets.LinkedList;
partial
class A
{
List y; // y has type Widgets.LinkedList
}
}
1.3 类成员
一个类的成员由两部分组成:由它的 class-member-declaration 引入的成员;从它的直接基类继承来的成员。
class-member-declarations:
class-member-declaration
class-member-declarations
class-member-declaration
class-member-declaration:
constant-declaration
field-declaration
method-declaration
property-declaration
event-declaration
indexer-declaration
operator-declaration
constructor-declaration
destructor-declaration
static-constructor-declaration
type-declaration
一个类类型的成员分为下列几种类别:
- 常量,表示与该类相关联的常量值(第 10.4 节)。
- 字段,即该类的变量(第 10.5 节)。
- 方法,用于实现可由该类执行的计算和操作(第 10.6 节)。
- 属性,用于定义一些命名特性以及与读取和写入这些特性相关的操作(第 10.7 节)。
- 事件,用于定义可由该类生成的通知(第 10.8 节)。
- 索引器,使该类的实例可按与数组相同的(语法)方式进行索引(第 10.9 节)。
- 运算符,用于定义表达式运算符,通过它对该类的实例进行运算(第 10.10 节)。
- 实例构造函数,用于实现初始化该类的实例所需的操作(第 10.11 节)
- 析构函数,用于实现在永久地放弃该类的一个实例之前要执行的操作(第 10.13 节)。
- 静态构造函数,用于实现初始化该类自身所需的操作(第 10.12 节)。
- 类型,用于表示一些类型,它们是该类的局部类型(第 10.3.8 节)。
可以包含可执行代码的成员统称为该类类型的 function members。类类型的函数成员包括:方法、属性、事件、索引器、运算符、实例构造函数、析构函数和该类类型的静态构造函数。
class-declaration 将创建一个新的声明空间(第 3.3 节),而直接包含在该 class-declaration 内的 class-member-declarations 将向此声明空间中引入新成员。下列规则适用于 class-member-declarations:
- 实例构造函数、静态构造函数和析构函数必须具有与直接包容它们的类相同的名称。所有其他成员的名称必须与该类的名称不同。
- 常量、字段、属性、事件或类型的名称必须不同于在同一个类中声明的所有其他成员的名称。
- 方法的名称必须不同于在同一个类中声明的所有其他非方法的名称。此外,必须不同于在同一类中声明的所有其他方法的签名(第 3.6 节),并且在同一类中声明的两种方法的签名不能只有 ref 和 out 不同。
- 实例构造函数的签名必须不同于在同一类中声明的所有其他实例的签名,并且在同一类中声明的两个构造函数的签名不能只有 ref 和 out 不同。
- 索引器的签名必须不同于在同一个类中声明的所有其他索引器的签名。
- 运算符的签名必须不同于在同一个类中声明的所有其他运算符的签名。
类类型的继承成员(第 10.3.3 节)不是类的声明空间的组成部分。因此,一个派生类可以使用与所继承的成员相同的名称或签名来声明自已的新成员(这同时也隐藏了被继承的同名成员)。
1.3.1 实例类型
每个类声明都有一个关联的绑定类型(第 4.4.3 节),即实例类型 (instance type)。对于泛型类声明,实例类型是通过从该类型声明创建构造类型(第 4.4 节)来构成的,所提供的每个类型实参替换对应的类型形参。由于实例类型使用类型形参,因此只能在类型形参的作用域中使用该实例类型;也就是在类声明的内部。对于在类声明中编写的代码,实例类型为 this 的类型。对于非泛型类,实例类型就是所声明的类。下面显示几个类声明以及它们的实例类型:
class A<T> // instance type: A<T>
{
class B {} // instance type: A<T>.B
class
C<U> {} // instance
type: A<T>.C<U>
}
class D {} // instance type: D
1.3.2 构造类型的成员
构造类型的非继承成员是通过将成员声明中的每个 type-parameter 替换为构造类型的对应 type-argument 来获得的。替换过程基于类型声明的语义含义,并不只是文本替换。
例如,给定下面的泛型类声明
class Gen<T,U>
{
public T[,] a;
public void G(int i, T t, Gen<U,T> gt) {...}
public U Prop { get {...} set {...} }
public
int H(double d) {...}
}
构造类型 Gen<int[],IComparable<string>> 具有以下成员:
public int[,][] a;
public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...}
public IComparable<string> Prop { get {...} set {...} }
public int H(double d) {...}
泛型类声明 Gen 中的成员 a 的类型是“T 的二维数组”,因此上面的构造类型中的成员 a 的类型是“int 的一维数组的二维数组”,或 int[,][]。
在实例函数成员中,类型 pe of this 是包含这些成员的声明的实例类型(第 10.3.1 节)。
泛型类的所有成员都可以直接或作为构造类型的一部分使用任何包容类 (enclosing class) 中的类型形参。当在运行时使用特定的封闭构造类型(第 4.4.2 节)时,所使用的每个类型形参都被替换成提供给该构造类型的实际类型实参。例如:
class C<V>
{
public V f1;
public C<V> f2 = null;
public
C(V x) {
this.f1 = x;
this.f2 = this;
}
}
class Application
{
static void Main() {
C<int> x1 = new
C<int>(1);
Console.WriteLine(x1.f1); // Prints 1
C<double>
x2 = new C<double>(3.1415);
Console.WriteLine(x2.f1); // Prints 3.1415
}
}
1.3.3 继承
一个类继承 (inherit) 它的直接基类类型的成员。继承意味着一个类隐式地将它的直接基类类型的所有成员当作自已的成员,但基类的实例构造函数、析构函数和静态构造函数除外。继承的一些重要性质为:
- 继承是可传递的。如果 C 从 B 派生,而 B 从 A 派生,则 C 将既继承在 B 中声明的成员,又继承在 A 中声明的成员。
- 派生类扩展它的直接基类。派生类能够在继承基类的基础上添加新的成员,但是它不能移除继承成员的定义。
- 实例构造函数、析构函数和静态构造函数是不可继承的,但所有其他成员是可继承的,无论它们所声明的可访问性(第 3.5 节)如何。但是,根据它们所声明的可访问性,有些继承成员在派生类中可能是无法访问的。
- 派生类可以通过声明具有相同名称或签名的新成员来隐藏 (hide)(第 3.7.1.2 节)那个被继承的成员。但是,请注意隐藏继承成员并不移除该成员,它只是使被隐藏的成员在派生类中不可直接访问。
- 类的实例含有在该类中以及它的所有基类中声明的所有实例字段集,并且存在一个从派生类类型到它的任一基类类型的隐式转换(第 6.1.6 节)。因此,可以将对某个派生类实例的引用视为对它的任一个基类实例的引用。
- 类可以声明虚的方法、属性和索引器,而派生类可以重写这些函数成员的实现。这使类展示出“多态性行为”特征,也就是说,同一个函数成员调用所执行的操作可能是不同的,这取决于用来调用该函数成员的实例的运行时类型。
构造类类型的继承成员是直接基类类型的成员(第 10.1.4.1 节),用构造类型的类型实参替换 base-class-specification 中出现的每个相应的类型形参,可以找到这些继承成员。反过来,通过将 base-class-specification 的相应 type-argument 替换为成员声明中的每个 type-parameter,又可以转换这些成员。
class B<U>
{
public U F(long index) {...}
}
class D<T>: B<T[]>
{
public T G(string s) {...}
}
在上面的示例中,构造类型 D<int> 具有一个非继承的成员 public int G(string s),该成员是通过将类型形参 T 替换为类型实参 int 来获得的。D<int> 还有一个从类声明 B 继承的成员。先用 int 替换基类说明 B<T[]> 中的 T 来确定 D<int> 的基类类型 B<int[]>,以此来确定该继承成员。然后,作为 B 的类型实参,int[] 将用来替换 public U F(long index) 中的 U,从而得到继承的成员 public int[] F(long index)。
1.3.4 new 修饰符
class-member-declaration 中可以使用与一个被继承的成员相同的名称或签名来声明一个成员。发生这种情况时,就称该派生类成员隐藏 (hide) 了基类成员。隐藏一个继承的成员不算是错误,但这确实会导致编译器发出警告。若要取消此警告,派生类成员的声明中可以包含一个 new 修饰符,表示派生成员是有意隐藏基成员的。第 3.7.1.2 节对本主题进行了进一步讨论。
如果在不隐藏所继承成员的声明中包含 new 修饰符,将对此状况发出警告。通过移除 new 修饰符可取消显示此警告。
1.3.5 访问修饰符
class-member-declaration 可以具有下列五种可能的已声明可访问性(第 3.5.1 节)中的任意一种:public、protected internal、protected、internal 或 private。除 protected internal 组合外,指定一个以上的访问修饰符会导致编译时错误。当 class-member-declaration 不包含任何访问修饰符时,假定为 private。
1.3.6 构成类型
在成员声明中所使用的类型称为成员的构成类型。可能的构成类型包括常量、字段、属性、事件或索引器类型,方法或运算符的返回类型,以及方法、索引器、运算符和实例构造函数的形参类型。成员的构成类型必须至少具有与该成员本身相同的可访问性(第 3.5.4 节)。
1.3.7 静态成员和实例成员
类的成员或者是静态成员 (static member),或者是实例成员 (instance member)。一般说来,可以这样来理解:静态成员属于类类型,而实例成员属于对象(类类型的实例)。
当字段、方法、属性、事件、运算符或构造函数声明中含有 static 修饰符时,它声明静态成员。此外,常量或类型声明会隐式地声明静态成员。静态成员具有下列特征:
- 在 E.M 形式的 member-access(第 7.6.4 节)中引用静态成员 M 时,E 必须表示含有 M 的那个类型。E 若表示一个实例,则会导致编译时错误。
- 静态字段只标识一个要由给定的封闭类类型的所有实例共享的存储位置。无论对一个给定的封闭式类类型创建了多少个实例,它的静态字段永远都只有一个副本。
- 静态函数成员(方法、属性、事件、运算符或构造函数)不能作用于具体的实例,在这类函数成员中引用 this 会导致编译时错误。
当字段、方法、属性、事件、索引器、构造函数或析构函数的声明中不包含 static 修饰符时,它声明实例成员。(实例成员有时称为非静态成员。)实例成员具有以下特点:
- 在 E.M 形式的 member-access(第 7.6.4 节)中引用实例成员 M 时,E 必须表示某个含有 M 的类型的一个实例。E 若表示类型,则会导致绑定时错误。
- 类的每个实例分别包含一组该类的所有实例字段。
- 实例函数成员(方法、属性、索引器、实例构造函数或析构函数)作用于类的给定实例,此实例可以用 this 访问(第 7.6.7 节)。
下列示例阐释访问静态和实例成员的规则:
class Test
{
int x;
static int y;
void F() {
x = 1; // Ok, same as this.x = 1
y = 1; // Ok, same as Test.y = 1
}
static void G() {
x = 1; // Error, cannot access this.x
y = 1; // Ok, same as Test.y = 1
}
static void Main() {
Test t = new Test();
t.x = 1; // Ok
t.y = 1; // Error, cannot access static member through instance
Test.x = 1; // Error, cannot access instance member through type
Test.y = 1; // Ok
}
}
F 方法显示,在实例函数成员中,simple-name(第 7.6.2 节)既可用于访问实例成员也可用于访问静态成员。G 方法显示,在静态函数成员中,通过 simple-name 访问实例成员会导致编译时错误。Main 方法显示,在 member-access(第 7.6.4 节)中,实例成员必须通过实例访问,静态成员必须通过类型访问。
1.3.8 嵌套类型
在类或结构声明内部声明的类型称为嵌套类型 (nested type)。在编译单元或命名空间内声明的类型称为非嵌套类型 (non-nested type)。
在下面的示例中
using System;
class A
{
class B
{
static void F() {
Console.WriteLine("A.B.F");
}
}
}
类 B 是嵌套类型,这是因为它在类 A 内声明,而由于类 A 在编译单元内声明,因此它是非嵌套类型。
1.3.8.1 完全限定名
嵌套类型的完全限定名(第 3.8.1 节)为 S.N,其中 S 是声明了 N 类型的那个类型的完全限定名。
1.3.8.2 已声明可访问性
非嵌套类型可以具有 public 或 internal 已声明可访问性,并且在默认情况下具有 internal 已声明可访问性。嵌套类型也可以具有上述两种声明可访问性,外加一种或更多种其他的声明可访问性,具体取决于包含它的那个类型是类还是结构:
- 在类中声明的嵌套类型可以具有五种形式的已声明可访问性(public、protected internal、protected、internal 或 private)中的任一种,而且与其他类成员一样,默认的已声明可访问性为 private。
- 在结构中声明的嵌套类型可以具有三种已声明可访问性(public、internal 或 private)中的任一种形式,而且与其他结构成员一样,默认的已声明可访问性为 private。
下面的示例
public class List
{
// Private data structure
private class Node
{
public object Data;
public Node Next;
public
Node(object data, Node next) {
this.Data = data;
this.Next = next;
}
}
private
Node first = null;
private Node last = null;
// Public interface
public void AddToFront(object o) {...}
public void AddToBack(object o) {...}
public object RemoveFromFront() {...}
public object RemoveFromBack() {...}
public
int Count { get {...} }
}
声明了一个私有嵌套类 Node。
1.3.8.3 隐藏
嵌套类型可以隐藏(第 3.7.1 节)基成员。对嵌套类型声明允许使用 new 修饰符,以便可以明确表示隐藏。下面的示例
using System;
class Base
{
public static void M() {
Console.WriteLine("Base.M");
}
}
class Derived: Base
{
new public class M
{
public static void F() {
Console.WriteLine("Derived.M.F");
}
}
}
class Test
{
static void Main() {
Derived.M.F();
}
}
演示嵌套类 M,该类隐藏了在 Base 中定义的方法 M。
1.3.8.4 this 访问
关于 this-access(第 7.6.7 节),嵌套类型和包含它的那个类型并不具有特殊的关系。准确地说,在嵌套类型内,this 不能用于引用包含它的那个类型的实例成员。当需要在嵌套类型内部访问包含它的那个类型的实例成员时,通过将代表所需实例的 this 作为一个实参传递给该嵌套类型的构造函数,就可以进行所需的访问了。以下示例
using System;
class C
{
int i = 123;
public
void F() {
Nested n = new Nested(this);
n.G();
}
public
class Nested
{
C this_c;
public
Nested(C c) {
this_c = c;
}
public
void G() {
Console.WriteLine(this_c.i);
}
}
}
class Test
{
static void Main() {
C c = new C();
c.F();
}
}
演示了此技巧。C 实例创建了一个 Nested 实例并将代表它自己的 this 传递给 Nested 的构造函数,这样,就可以对 C 的实例成员进行后续访问了。
1.3.8.5 对包含类型的私有和受保护成员的访问
嵌套类型可以访问包含它的那个类型可访问的所有成员,包括该类型自己的具有 private 和 protected 声明可访问性的成员。下面的示例
using System;
class C
{
private static void F() {
Console.WriteLine("C.F");
}
public
class Nested
{
public static void G() {
F();
}
}
}
class Test
{
static void Main() {
C.Nested.G();
}
}
演示包含有嵌套类 Nested 的类 C。在 Nested 内,方法 G 将调用在 C 中定义的静态方法 F,而 F 具有 private 已声明可访问性。
嵌套类型还可以访问在包含它的那个类型的基类型中定义的受保护成员。在下面的示例中
using System;
class Base
{
protected void F() {
Console.WriteLine("Base.F");
}
}
class Derived: Base
{
public class Nested
{
public void G() {
Derived d = new Derived();
d.F(); // ok
}
}
}
class Test
{
static void Main() {
Derived.Nested n = new
Derived.Nested();
n.G();
}
}
嵌套类 Derived.Nested 通过对一个 Derived 的实例进行调用,访问在 Derived 的基类 Base 中定义的受保护方法 F。
1.3.8.6 泛型类中的嵌套类型
泛型类声明可以包含嵌套的类型声明。包容类的类型形参可以在嵌套类型中使用。嵌套类型声明可以包含仅适用于该嵌套类型的附加类型形参。
泛型类声明中包含的每个类型声明都隐式地是泛型类型声明。在编写对嵌套在泛型类型中的类型的引用时,必须指定其包容构造类型(包括其类型实参)。但是可在外层类中不加限定地使用嵌套类型;在构造嵌套类型时可以隐式地使用外层类的实例类型。下面的示例演示三种不同的引用从 Inner 创建的构造类型的正确方法;前两种方法是等效的:
class
Outer<T>
{
class Inner<U>
{
public static void F(T t, U u) {...}
}
static void F(T t) {
Outer<T>.Inner<string>.F(t,
"abc"); // These two
statements have
Inner<string>.F(t,
"abc"); // the same
effect
Outer<int>.Inner<string>.F(3, "abc"); // This type is different
Outer.Inner<string>.F(t,
"abc"); // Error, Outer
needs type arg
}
}
嵌套类型中的类型形参可以隐藏外层类型中声明的成员或类型形参,但这是一种不好的编程风格:
class
Outer<T>
{
class Inner<T> // Valid, hides Outer’s T
{
public T t; // Refers to Inner’s T
}
}
1.3.9 保留成员名称
为便于基础 C# 运行时的实现,对于每个属性、事件或索引器的源成员声明,实现都必须根据成员声明的种类、名称和类型保留两个方法签名。如果程序声明一个成员,而该成员的签名与这些保留签名之一匹配,那么即使基础运行时实现不使用这些保留签名,仍会出现编译时错误。
保留名称不会引入声明,因此它们不参与成员查找。但是,一个声明的关联的保留方法签名的确参与继承(第 10.3.3 节),而且可以使用 new 修饰符(第 10.3.4 节)隐藏起来。
保留这些名称有三个目的:
- 使基础的实现可以通过将普通标识符用作一个方法名称,从而对 C# 语言的功能进行 get 或 set 访问。
- 使其他语言可以通过将普通标识符用作一个方法名称,对 C# 语言的功能进行 get 或 set 访问,从而实现交互操作。
- 使保留成员名称的细节在所有的 C# 实现中保持一致,这有助于确保被一个符合本规范的编译器所接受的源程序也可被另一个编译器接受。
析构函数(第 10.13 节)的声明也会导致一个签名被保留(第 10.3.9.4 节)。
1.3.9.1 为属性保留的成员名称
对于类型 T 的属性 P(第 10.7 节),保留了下列签名:
T get_P();
void set_P(T value);
即使该属性是只读或者只写的,这两个签名仍然都被保留。
在下面的示例中
using System;
class A
{
public int P {
get { return 123; }
}
}
class B: A
{
new public int get_P() {
return 456;
}
new
public void set_P(int value) {
}
}
class Test
{
static void Main() {
B b = new B();
A a = b;
Console.WriteLine(a.P);
Console.WriteLine(b.P);
Console.WriteLine(b.get_P());
}
}
类 A 定义了只读属性 P,从而保留了 get_P 和 set_P 方法的签名。类 B 从 A 派生并隐藏了这两个保留的签名。此例产生输出:
123
123
456
1.3.9.2 为事件保留的成员名称
对于委托类型 T 的事件 E(第 10.8 节),保留了下列签名:
void add_E(T handler);
void remove_E(T handler);
1.3.9.3 为索引器保留的成员名称
对于类型 T 的且具有形参列表 L 的索引器(第 10.9 节),保留了下列签名:
T get_Item(L);
void set_Item(L, T value);
即使索引器是只读或者只写的,这两个签名仍然都被保留。
此外,保留成员名称 Item。
1.3.9.4 为析构函数保留的成员名称
对于包含析构函数(第 10.13 节)的类,保留了下列签名:
void Finalize();
1.4 常量
常量 (constant) 是表示常量值(即,可以在编译时计算的值)的类成员。constant-declaration 可引入一个或多个给定类型的常量。
constant-declaration:
attributesopt constant-modifiersopt const
type constant-declarators ;
constant-modifiers:
constant-modifier
constant-modifiers constant-modifier
constant-modifier:
new
public
protected
internal
private
constant-declarators:
constant-declarator
constant-declarators , constant-declarator
constant-declarator:
identifier = constant-expression
constant-declaration 可包含一组 attributes(第 17 章)、一个 new 修饰符(第 10.3.4 节)和一个由四个访问修饰符构成的有效组合(第 10.3.5 节)。特性和修饰符适用于所有由 constant-declaration 所声明的成员。虽然常量被认为是静态成员,但在 constant-declaration 中既不要求也不允许使用 static 修饰符。同一个修饰符在一个常量声明中多次出现是错误的。
constant-declaration 的 type 用于指定由声明引入的成员的类型。类型后接一个 constant-declarator 列表,该列表中的每个声明符引入一个新成员。constant-declarator 包含一个用于命名该成员的 identifier,后接一个“=”标记,然后跟一个对该成员赋值的 constant-expression(第 7.19 节)。
在常量声明中指定的 type 必须是 sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool、string、 enum-type 或 reference-type。每个 constant-expression 所产生的值必须属于目标类型,或者可以通过一个隐式转换(第 6.1) 节)转换为目标类型。
常量的 type 必须至少与常量本身(第 3.5.4 节)具有同样的可访问性。
使用 simple-name(第 7.6.2 节)或 member-access(第 7.6.4 节)从表达式获取常量的值。
常量本身可以出现在 constant-expression 中。因此,常量可用在任何需要 constant-expression 的构造中。这样的构造示例包括 case 标签、goto case 语句、enum 成员声明、属性和其他的常量声明。
如第 7.19 节中所描述,constant-expression 是在编译时就可以完全计算出来的表达式。由于创建 string 以外的 reference-type 的非 null 值的唯一方法是应用 new 运算符,但 constant-expression 中不允许使用 new 运算符,因此,除 string 以外的 reference-types 常量的唯一可能的值是 null。
如果需要一个具有常量值的符号名称,但是该值的类型不允许在常量声明中使用,或在编译时无法由 constant-expression 计算出该值,则可以改用 readonly 字段(第 10.5.2 节)。
声明了多个常量的一个常量声明等效于具有相同特性、修饰符和类型的多个常量的声明,其中每个声明均只声明一个常量。例如
class A
{
public const double X = 1.0, Y = 2.0, Z =
3.0;
}
相当于
class A
{
public const double X = 1.0;
public const double Y = 2.0;
public const double Z = 3.0;
}
一个常量可以依赖于同一程序内的其他常量,只要这种依赖关系不是循环的。编译器会自动地安排适当的顺序来计算各个常量声明。在下面的示例中
class A
{
public const int X = B.Z + 1;
public const int Y = 10;
}
class B
{
public const int Z = A.Y + 1;
}
编译器首先计算 A.Y,然后计算 B.Z,最后计算 A.X,产生值 10、11 和 12。常量声明也可以依赖于其他程序中的常量,但这种依赖关系只能是单方向的。上面的示例中,如果 A 和 B 在不同的程序中声明,A.X 可以依赖于 B.Z,但是 B.Z 就无法同时再依赖于 A.Y 了。
1.5 字段
字段 (field) 是一种表示与对象或类关联的变量的成员。field-declaration 用于引入一个或多个给定类型的字段。
field-declaration:
attributesopt
field-modifiersopt
type variable-declarators ;
field-modifiers:
field-modifier
field-modifiers field-modifier
field-modifier:
new
public
protected
internal
private
static
readonly
volatile
variable-declarators:
variable-declarator
variable-declarators , variable-declarator
variable-declarator:
identifier
identifier = variable-initializer
variable-initializer:
expression
array-initializer
field-declaration 可以包含一组 attributes(第 17 章),一个 new 修饰符(第 10.3.4 节),由四个访问修饰符组成的一个有效组合(第 10.3.5 节)和一个 static 修饰符(第 10.5.1 节)。此外,field-declaration 可以包含一个 readonly 修饰符(第 10.5.2 节)或一个 volatile 修饰符(第 10.5.3 节),但不能同时包含这两个修饰符。特性和修饰符适用于由该 field-declaration 所声明的所有成员。同一个修饰符在一个字段声明中多次出现是错误的。
field-declaration 的 type 用于指定由该声明引入的成员的类型。类型后接一个 variable-declarator 列表,其中每个变量声明符引入一个新成员。variable-declarator 包含一个用于命名该成员的 identifier,还可以根据需要再后接一个“=”标记,以及一个用于赋予成员初始值的 variable-initializer(第 10.5.5 节)。
字段的 type 必须至少与字段本身(第 3.5.4 节)具有同样的可访问性。
使用 simple-name(第 7.6.2 节)或 member-access(第 7.6.4 节)从表达式获得字段的值。使用 assignment(第 7.17 节)修改非只读字段的值。可以使用后缀增量和减量运算符(第 7.6.9 节)以及前缀增量和减量运算符(第 7.7.5 节)获取和修改非只读字段的值。
声明了多个字段的一个字段声明等效于具有相同特性、修饰符和类型的多个字段的声明,其中每个声明均只声明一个字段。例如
class A
{
public static int X = 1, Y, Z = 100;
}
相当于
class A
{
public static int X = 1;
public static int Y;
public static int Z = 100;
}
1.5.1 静态字段和实例字段
当一个字段声明中含有 static 修饰符时,由该声明引入的字段为静态字段 (static field)。当不存在 static 修饰符时,由该声明引入的字段为 iinstance fields。静态字段和实例字段是 C# 所支持的几种变量(第 5 章)中的两种,它们有时被分别称为静态变量 (static variable) 和实例变量 (instance variable)。
静态字段不是特定实例的一部分,而是在封闭类型的所有实例之间共享(第 4.4.2 节)。不管创建了多少个封闭式类类型的实例,对于关联的应用程序域来说,在任何时候静态字段都只会有一个副本。
例如:
class
C<V>
{
static int count = 0;
public C() {
count++;
}
public static int Count {
get { return count; }
}
}
class
Application
{
static void Main() {
C<int> x1 = new C<int>();
Console.WriteLine(C<int>.Count); // Prints 1
C<double> x2 = new C<double>();
Console.WriteLine(C<int>.Count); // Prints 1
C<int> x3 = new C<int>();
Console.WriteLine(C<int>.Count); // Prints 2
}
}
实例字段属于某个实例。具体而言,类的每个实例都包含了该类的所有实例字段的一个单独的集合。
若用 E.M 形式的 member-access(第 7.6.4 节)来引用一个字段,如果 M 是静态字段,则 E 必须表示含有 M 的一个类型,但如果 M 是实例字段,则 E 必须表示一个含有 M 的类型的某个实例。
第 10.3.7 节对静态成员和实例成员之间的差异进行了进一步讨论。
1.5.2 只读字段
当 field-declaration 中含有 readonly 修饰符时,该声明所引入的字段为只读字段 (readonly field)。给只读字段的直接赋值只能作为声明的组成部分出现,或在同一类中的实例构造函数或静态构造函数中出现。(在这些上下文中,只读字段可以被多次赋值。)准确地说,只在下列上下文中允许对 readonly 字段进行直接赋值:
- 在用于引入该字段的 variable-declarator 中(通过在声明中包括一个 variable-initializer)。
- 对于实例字段,在包含字段声明的类的实例构造函数中;对于静态字段,在包含字段声明的类的静态构造函数中。这些也是可以将 readonly 字段作为 out 或 ref 形参进行传递的仅有的上下文。
在其他任何上下文中,试图对 readonly 字段进行赋值或将它作为 out 或 ref 形参传递都会导致编译时错误。
1.5.2.1 对常量使用静态只读字段
如果需要一个具有常量值的符号名称,但该值的类型不允许在 const 声明中使用,或者无法在编译时计算出该值,则 static readonly 字段就可以发挥作用了。在下面的示例中
public class Color
{
public static readonly Color Black = new
Color(0, 0, 0);
public static readonly Color White = new
Color(255, 255, 255);
public static readonly Color Red = new
Color(255, 0, 0);
public static readonly Color Green = new
Color(0, 255, 0);
public static readonly Color Blue = new
Color(0, 0, 255);
private byte red, green, blue;
public
Color(byte r, byte g, byte b) {
red = r;
green = g;
blue = b;
}
}
Black、White、Red、Green 和 Blue 成员不能声明为 const 成员,这是因为在编译时无法计算它们的值。不过,改为将它们声明为 static readonly 能达到基本相同的效果。
1.5.2.2 常量和静态只读字段的版本控制
常量和只读字段具有不同的二进制版本控制语义。当表达式引用常量时,该常量的值在编译时获取,但是当表达式引用只读字段时,要等到运行时才获取该字段的值。请考虑一个包含两个单独程序的应用程序:
using System;
namespace Program1
{
public class Utils
{
public static readonly int X = 1;
}
}
namespace Program2
{
class Test
{
static void Main() {
Console.WriteLine(Program1.Utils.X);
}
}
}
Program1 和 Program2 命名空间表示两个单独编译的程序。由于 Program1.Utils.X 声明为静态只读字段,因此 Console.WriteLine 语句要输出的值在编译时是未知的,直到在运行时才能获取。因此,如果更改了 X 的值并重新编译 Program1,则 Console.WriteLine 语句会输出新值,即使未重新编译 Program2 也是如此。但如果 X 为常量,则 X 的值已在编译 Program2 时获取,则该值仍不会受到 Program1 中更改的影响,除非重新编译 Program2。
1.5.3 可变字段
当 field-declaration 中含有 volatile 修饰符时,该声明引入的字段为可变字段 (volatile field)。
由于采用了优化技术(它会重新安排指令的执行顺序),在多线程的程序运行环境下,如果不采取同步(如由 lock-statement(第 8.12 节)所提供的)控制手段,则对于非可变字段的访问可能会导致意外的和不可预见的结果。这些优化可以由编译器、运行时系统或硬件执行。但是,对于可变字段,优化时的这种重新排序必须遵循以下规则:
- 读取一个可变字段称为可变读取 (volatile read)。可变读取具有“获取语义”;也就是说,按照指令序列,所有排在可变读取之后的对内存的引用,在执行时也一定排在它的后面。
- 写入一个可变字段称为可变写入 (volatile write)。可变写入具有“释放语义”;也就是说,按照指令序列,所有排在可变写入之前的对内存的引用,在执行时也一定排在它的前面。
这些限制能确保所有线程都会观察到由其他任何线程所执行的可变写入(按照原来安排的顺序)。一个遵循本规范的实现并非必须做到:使可变写入的执行顺序,在所有正在执行的线程看来都是一样的。可变字段的类型必须是下列类型中的一种:
- reference-type。
- 类型 byte、sbyte、short、ushort、int、uint、char、float、bool、System.IntPtr 或 System.UIntPtr。
- 枚举基类型为 byte、sbyte、short、ushort、int 或 uint 的 enum-type。
下面的示例
using System;
using System.Threading;
class Test
{
public static int result;
public static volatile bool finished;
static
void Thread2() {
result = 143;
finished = true;
}
static
void Main() {
finished = false;
//
Run Thread2() in a new thread
new Thread(new
ThreadStart(Thread2)).Start();
//
Wait for Thread2 to signal that it has a result by setting
// finished to true.
for (;;) {
if (finished) {
Console.WriteLine("result
= {0}", result);
return;
}
}
}
}
产生下列输出:
result = 143
在本示例中,方法 Main 将启动一个新线程,该线程运行方法 Thread2。该方法将一个值存储在叫做 result 的非可变字段中,然后将 true 存储在可变字段 finished 中。主线程将等待字段 finished 被设置为 true,然后读取字段 result。由于已将 finished 声明为 volatile,主线程必须从字段 result 读取值 143。如果字段 finished 未被声明为 volatile,则存储 finished 之后,主线程可看到存储 result,因此主线程从字段 result 读取值 0。将 finished 声明为 volatile 字段可以防止任何此类不一致性。
1.5.4 字段初始化
字段(无论是静态字段还是实例字段)的初始值都是字段的类型的默认值(第 5.2 节)。在此默认初始化发生之前不可能看到字段的值,因此字段永远不会是“未初始化的”。下面的示例
using System;
class Test
{
static bool b;
int i;
static
void Main() {
Test t = new Test();
Console.WriteLine("b = {0}, i =
{1}", b, t.i);
}
}
产生输出
b = False, i = 0
这是因为 b 和 i 都被自动初始化为默认值。
1.5.5 变量初始值设定项
字段声明可能包括 variable-initializer。对于静态字段,变量初始值设定项相当于在类初始化期间执行的赋值语句。对于实例字段,变量初始值设定项相当于创建类的实例时执行的赋值语句。
下面的示例
using System;
class Test
{
static double x = Math.Sqrt(2.0);
int i = 100;
string s = "Hello";
static
void Main() {
Test a = new Test();
Console.WriteLine("x = {0}, i =
{1}, s = {2}", x, a.i, a.s);
}
}
产生输出
x = 1.4142135623731, i = 100, s = Hello
这是因为对 x 的赋值发生在静态字段初始值设定项执行时,而对 i 和 s 的赋值发生在实例字段初始值设定项执行时。
第 10.5.4 节中描述的默认值初始化对所有字段都发生,包括具有变量初始值设定项的字段。因此,当初始化一个类时,首先将该类中的所有静态字段初始化为它们的默认值,然后以文本顺序执行各个静态字段初始值设定项。与此类似,创建类的一个实例时,首先将该实例中的所有实例字段初始化为它们的默认值,然后以文本顺序执行各个实例字段初始值设定项。
在具有变量初始值设定项的静态字段处于默认值状态时,也有可能访问它们。但是,为了培养良好的编程风格,强烈建议不要这么做。下面的示例
using System;
class Test
{
static int a = b + 1;
static int b = a + 1;
static
void Main() {
Console.WriteLine("a = {0}, b =
{1}", a, b);
}
}
演示此行为。尽管 a 和 b 的定义是循环的,此程序仍是有效的。它产生以下输出:
a = 1, b = 2
这是因为静态字段 a 和 b 在它们的初始值设定项执行之前被初始化为 0(int 的默认值)。当 a 的初始值设定项运行时,b 的值为零,所以 a 被初始化为 1。当 b 的初始值设定项运行时,a 的值已为 1,所以 b 被初始化为 2。
1.5.5.1 静态字段初始化
类的静态字段变量初始值设定项对应于一个赋值序列,这些赋值按照它们在类声明中出现的文本顺序执行。如果类中存在静态构造函数(第 10.12 节) \r \h ,则在静态构造函数即将执行之前,将执行静态字段初始值设定项。否则,静态字段初始值设定项在第一次使用该类的静态字段之前先被执行,但实际执行时间依赖于具体的实现。下面的示例
using System;
class Test
{
static void Main() {
Console.WriteLine("{0}
{1}", B.Y, A.X);
}
public
static int F(string s) {
Console.WriteLine(s);
return 1;
}
}
class A
{
public static int X = Test.F("Init
A");
}
class B
{
public static int Y = Test.F("Init
B");
}
或者产生如下输出:
Init A
Init B
1 1
或者产生如下输出:
Init B
Init A
1 1
这是因为 X 的初始值设定项和 Y 的初始值设定项的执行顺序无法预先确定,上述两种顺序都有可能发生;唯一能够确定的是:它们一定会在对那些字段的引用之前发生。但是,下面的示例:
using System;
class Test
{
static void Main() {
Console.WriteLine("{0}
{1}", B.Y, A.X);
}
public
static int F(string s) {
Console.WriteLine(s);
return 1;
}
}
class A
{
static A() {}
public
static int X = Test.F("Init A");
}
class B
{
static B() {}
public
static int Y = Test.F("Init B");
}
所产生的输出必然是:
Init B
Init A
1 1
这是因为关于何时执行静态构造函数的规则(在第 10.12 节中定义)进行了这样的规定:B 的静态构造函数(以及 B 的静态字段初始值设定项)必须在 A 的静态构造函数和字段初始值设定项之前运行。
1.5.5.2 实例字段初始化
类的实例字段变量初始值设定项对应于一个赋值序列,它在当控制进入该类的任一个实例构造函数(第 10.11.1 节)时立即执行。这些变量初始值设定项按它们出现在类声明中的文本顺序执行。第 10.11 节中对类实例的创建和初始化过程进行了进一步描述。
实例字段的变量初始值设定项不能引用正在创建的实例。因此,在变量初始值设定项中引用 this 是编译时错误,同样,在变量初始值设定项中通过 simple-name 引用任何一个实例成员也是一个编译时错误。在下面的示例中
class A
{
int x = 1;
int y = x + 1; // Error, reference to instance member of this
}
y 的变量初始值设定项导致编译时错误,原因是它引用了正在创建的实例的成员。
1.6 方法
方法 (method) 是一种成员,用于实现可由对象或类执行的计算或操作。方法是使用 method-declaration 来声明的:
method-declaration:
method-header method-body
method-header:
attributesopt
method-modifiersopt partialopt
return-type member-name type-parameter-listopt
(
formal-parameter-listopt
)
type-parameter-constraints-clausesopt
method-modifiers:
method-modifier
method-modifiers method-modifier
method-modifier:
new
public
protected
internal
private
static
virtual
sealed
override
abstract
extern
async
return-type:
type
void
member-name:
identifier
interface-type . identifier
method-body:
block
;
method-declaration 可包含一组 attributes(第 17 章)和一个由四个访问修饰符构成的有效组合(第 10.3.5 节),new (第 10.3.4 节)、static(第 10.6.2 节)、virtual(第 10.6.3 节)、override(第 10.6.4 节)、sealed(第 10.6.5 节)、abstract(第 10.6.6 节)和 extern(第 10.6.7 节)。
如果以下所有条件为真,则所述的声明就具有一个有效的修饰符组合:
- 该声明包含一个由访问修饰符(第 10.3.5 节)组成的有效组合。
- 该声明中所含的修饰符没有彼此相同的。
- 该声明最多包含下列修饰符中的一个: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”修饰符的方法是一种异步函数,并遵循第 10.14 节中描述的规则。
方法声明的 return-type 用于指定由该方法计算和返回的值的类型。如果方法不返回一个值,则它的 return-type 为 void。如果声明包含 partial 修饰符,则返回类型必须为 void。
member-name 用于指定方法的名称。除非方法是一个显式接口成员的实现(第 13.4.1 节),否则 member-name 仅是一个 identifier。对于显式接口成员实现,member-name 由一个 interface-type 并后接一个“.”和一个 identifier 组成。
可选的 type-parameter-list 用于指定方法的类型形参(第 10.1.3 节)。如果指定了 type-parameter-list,则方法是泛型方法 (generic method)。如果方法具有 extern 修饰符,则不能指定 type-parameter-list。
可选的 formal-parameter-list 用于指定方法的形参(第 10.6.1 节)。
可选的 type-parameter-constraints-clauses 用于指定对各个类型形参(第 10.1.5 节)的约束,仅在同时提供了 type-parameter-list 的情况下才可以指定,该方法没有 override 修饰符。
return-type 和在方法的 formal-parameter-list 中引用的各个类型必须至少具有和方法本身相同的可访问性(第 3.5.4 节)。
对于 abstract 和 extern 方法,method-body 只由一个分号组成。对于 partial 方法,method-body 由一个分号或由一个 block 组成。对于所有其他方法,method-body 由一个 block 组成,该块用于指定在调用方法时要执行哪些语句。
如果 method-body 包含分号,则声明可能不包含 async 修饰符。
一个方法的名称、类型形参列表和形参表定义了该方法的签名(第 3.6 节)。准确地说,一个方法的签名由它的名称、类型形参的数目以及它的形参的数目、修饰符和类型组成。为此,出现在形参类型中的方法的任何类型形参都不按名称标识,而是按其在方法的类型实参列表中的序号位置标识。返回类型不是方法签名的一部分,类型形参或形参的名称也不是。
方法的名称必须不同于在同一个类中声明的所有其他非方法的名称。此外,必须不同于在同一类中声明的所有其他方法的签名,并且在同一类中声明的两种方法的签名不能只有 ref 和 out 不同。
方法的 type-parameter 作用于整个 method-declaration 范围,并且可在整个该范围中用于构成 return-type、method-body 和 type-parameter-constraints-clauses(但是不包括 attributes)中的类型。
所有形参和类型形参都不能同名。
1.6.1 方法形参
一个方法的形参(如果有)是由该方法的 formal-parameter-list 来声明的。
formal-parameter-list:
fixed-parameters
fixed-parameters , parameter-array
parameter-array
fixed-parameters:
fixed-parameter
fixed-parameters , fixed-parameter
fixed-parameter:
attributesopt
parameter-modifieropt
type identifier default-argumentopt
default-argument:
= expression
parameter-modifier:
ref
out
this
parameter-array:
attributesopt params array-type identifier
形参表包含一个或多个由逗号分隔的形参,其中只有最后一个形参才可以是 parameter-array。
fixed-parameter 包含一组可选 attributes(第 17 节)、一个可选的 ref、out 或 this 修饰符、一个 type、一个 identifier 以及一个可选的 default-argument。每个 fixed-parameter 均声明了一个形参,指定了该形参的名称及其所属的类型。this 修饰符将方法指定为扩展方法,仅允许在静态方法的第一个形参上使用该修饰符。第 10.6.9 节中对扩展方法进行了进一步描述。
具有 default-argument 的 fixed-parameter 称为可选形参 (optional parameter),没有default-argument 的 fixed-parameter 称为必选形参 (required parameter)。在 formal-parameter-list 中,必选形参不能出现在可选形参之后。
ref 或 out 形参不能有 default-argument。default-argument 中的 expression 必须为下列各项之一:
- constant-expression
- new S() 形式的表达式,其中 S 是值类型
- default(S) 形式的表达式,其中 S 是值类型
expression 必须能够由标识或不可为 null 的转换隐式转换为形参的类型。
如果实现分步方法声明(第 10.2.7 节)、显式接口成员实现(第 13.4.1 节)或单参数索引器声明(第 10.9 节)中出现可选参数,编译器应发出警告,因为这些成员不能以允许忽略实参的方式调用。
parameter-array 包含一组可选的 attributes(第 17 章)、一个 params 修饰符、一个 array-type 和一个 identifier。形参数组声明单个具有给定名称且属于给定数组类型的形参。形参数组中的 array-type 必须是一维数组类型(第 12.1 节)。在方法调用中,形参数组可以用单个给定数组类型的实参来指定,也可以用零个或多个该数组元素类型的实参来指定。第 10.6.1.4 节中对形参数组进行了进一步描述。
parameter-array 可以出现在可选形参之后,但不能具有默认值,如果忽略 parameter-array 的实参,则会创建一个空数组。
以下示例说明不同种类的形参:
public void
M(
ref int i,
decimal d,
bool b = false,
bool? n = false,
string s = "Hello",
object o = null,
T t = default(T),
params int[] a
) { }
在 M 的 formal-parameter-list 中,i 是必需的引用参数,d 是必需的值参数,b、s、o 和 t 是可选的值参数,a 是参数数组。
方法声明为所声明的形参、类型形参和局部变量创建单独的声明空间。该方法的类型形参表与形参表和在该方法的 block 中的局部变量声明将它们所声明的名称提供给此声明空间。如果方法声明空间的两个成员具有相同的名称,则会发生错误。如果方法声明空间和嵌套的声明空间的局部变量声明空间包含同名的元素,则会发生错误。
执行一个方法调用(第 7.6.5.1 节)时,创建关于该方法的形参和局部变量的一个副本(仅适用于本次调用),而该调用所提供的实参列表则用于将所含的值或变量引用赋给新创建的形参。在方法的 block 内,形参可以在 simple-name 表达式(第 7.6.2 节)中由它们的标识符引用。
有四种形参:
- 值形参,声明时不带任何修饰符。
- 引用形参,用 ref 修饰符声明。
- 输出形参,用 out 修饰符声明。
- 形参数组,用 params 修饰符声明。
如第 3.6 节中所述,ref 和 out 修饰符是方法签名的组成部分,但 params 修饰符不是。
1.6.1.1 值参数
声明时不带修饰符的形参是值形参。一个值形参对应于一个局部变量,只是它的初始值来自该方法调用所提供的相应实参。
当形参是值形参时,方法调用中的对应实参必须是表达式,并且它的类型可以隐式转换(第 6.1 节)为形参的类型。
允许方法将新值赋给值参数。这样的赋值只影响由该值形参表示的局部存储位置,而不会影响在方法调用时由调用方给出的实参。
1.6.1.2 引用参数
引用形参是用 ref 修饰符声明的形参。与值形参不同,引用形参并不创建新的存储位置。相反,引用形参表示的存储位置恰是在方法调用中作为实参给出的那个变量所表示的存储位置。
当形参为引用形参时,方法调用中的对应实参必须由关键字 ref 并后接一个与形参类型相同的 variable-reference(第 5.3.3 节)组成。变量在可以作为引用形参传递之前,必须先明确赋值。
在方法内部,引用形参始终被认为是明确赋值的。
声明为迭代器(第 10.14 节)的方法不能有引用形参。
下面的示例
using System;
class Test
{
static void Swap(ref int x, ref int y) {
int temp = x;
x = y;
y = temp;
}
static void Main() {
int i = 1, j = 2;
Swap(ref i, ref j);
Console.WriteLine("i = {0}, j =
{1}", i, j);
}
}
产生输出
i = 2, j = 1
对于对 Swap 的调用(在 Main 中),x 表示 i,y 表示 j。因此,该调用具有交换 i 和 j 的值的效果。
在采用引用形参的方法中,多个名称可能表示同一存储位置。在下面的示例中
class A
{
string s;
void F(ref string a, ref string b) {
s = "One";
a = "Two";
b = "Three";
}
void G() {
F(ref s, ref s);
}
}
在 G 中调用 F 时,分别为 a 和 b 传递了一个对 s 的引用。因此,对于该调用,名称 s、a 和 b 全都引用同一存储位置,并且三个赋值全都修改了同一个实例字段 s。
1.6.1.3 输出形参
用 out 修饰符声明的形参是输出形参。类似于引用形参,输出形参不创建新的存储位置。相反,输出形参表示的存储位置恰是在该方法调用中作为实参给出的那个变量所表示的存储位置。
当形参为输出形参时,方法调用中的相应实参必须由关键字 out 并后接一个与形参类型相同的 variable-reference(第 5.3.3 节)组成。变量在可以作为输出形参传递之前不一定需要明确赋值,但是在将变量作为输出形参传递的调用之后,该变量被认为是明确赋值的。
在方法内部,与局部变量相同,输出形参最初被认为是未赋值的,因而必须在使用它的值之前明确赋值。
在方法返回之前,该方法的每个输出形参都必须明确赋值。
声明为分部方法(第 10.2.7 节)或迭代器(第 10.14 节)的方法不能有输出形参。
输出形参通常用在需要产生多个返回值的方法中。例如:
using System;
class Test
{
static void SplitPath(string path, out
string dir, out string name) {
int i = path.Length;
while (i > 0) {
char ch = path[i – 1];
if (ch == '\\' || ch == '/' || ch
== ':') break;
i--;
}
dir = path.Substring(0, i);
name = path.Substring(i);
}
static void Main() {
string dir, name;
SplitPath("c:\\Windows\\System\\hello.txt",
out dir, out name);
Console.WriteLine(dir);
Console.WriteLine(name);
}
}
此例产生输出:
c:\Windows\System\
hello.txt
请注意,dir 和 namename变量在它们被传递给 SplitPath 之前可以是未赋值的,而它们在调用之后就被认为是明确赋值的了。
1.6.1.4 形参数组
用 params 修饰符声明的形参是形参数组。如果形参表包含一个形参数组,则该形参数组必须位于该列表的最后而且它必须是一维数组类型。例如,类型 string[] 和 string[][] 可用作形参数组的类型,但类型 string[,] 不能。不能将 params 修饰符与 ref 和 out 修饰符组合起来使用。
在一个方法调用中,允许以下列两种方式之一来为形参数组指定对应的实参:
- 为形参数组提供的实参可以是单个表达式,它的类型可以隐式转换(第 6.1 节)为形参数组的类型。在此情况下,形参数组的作用与值形参完全一样。
- 此外,调用时可以为形参数组指定零个或多个实参,其中每个实参都是一个表达式,它的类型可隐式转换(第 6.1 节)为形参数组的元素的类型。在此情况下,调用会创建一个该形参数组类型的实例,其所含的元素个数等于给定的实参个数,再用给定的实参值初始化此数组实例的每个元素,然后将新创建的数组实例用作实参。
除了允许在调用中使用可变数量的实参,形参数组与同一类型的值形参(第 10.6.1.1 节)完全等效。
下面的示例
using System;
class Test
{
static void F(params int[] args) {
Console.Write("Array contains
{0} elements:", args.Length);
foreach (int i in args)
Console.Write(" {0}",
i);
Console.WriteLine();
}
static void Main() {
int[] arr = {1, 2, 3};
F(arr);
F(10, 20, 30, 40);
F();
}
}
产生输出
Array
contains 3 elements: 1 2 3
Array contains 4 elements: 10 20 30 40
Array contains 0 elements:
F 的第一次调用只是将数组 a 作为值形参传递。F 的第二次调用自动创建一个具有给定元素值的四元素 int[] 并将该数组实例作为值形参传递。与此类似,F 的第三次调用创建一个零元素的 int[] 并将该实例作为值形参传递。第二次和第三次调用完全等效于编写下列代码:
F(new int[]
{10, 20, 30, 40});
F(new int[] {});
执行重载决策时,具有形参数组的方法的正常形式或扩展形式(第 7.5.3.1 节)都是适用的。只有在方法的正常形式不适用,并且在同一类型中尚未声明与方法的扩展形式具有相同签名的适用方法时,上述的方法扩展形式才可供选用。
下面的示例
using System;
class Test
{
static void F(params object[] a) {
Console.WriteLine("F(object[])");
}
static void F() {
Console.WriteLine("F()");
}
static void F(object a0, object a1) {
Console.WriteLine("F(object,object)");
}
static void Main() {
F();
F(1);
F(1, 2);
F(1, 2, 3);
F(1, 2, 3, 4);
}
}
产生输出
F();
F(object[]);
F(object,object);
F(object[]);
F(object[]);
在该示例中,在同一个类中,已经声明了两个常规方法,它们的签名与具有形参数组的那个方法的扩展形式相同。因此,在执行重载决策时不考虑这些扩展形式,因而第一次和第三次方法调用将选择常规方法。当在某个类中声明了一个具有形参数组的方法时,同时再声明一些与该方法的扩展形式具有相同的签名的常规方法,这种情况比较常见。这样做可以避免为数组配置内存空间(若调用具有形参数组的方法的扩展形式,则无法避免)。
当形参数组的类型为 object[] 时,在方法的正常形式和单个 object 形参的扩展形式之间可能产生潜在的多义性。产生此多义性的原因是 object[] 本身可隐式转换为 object。然而,此多义性并不会造成任何问题,这是因为可以在需要时通过插入一个强制转换来解决它。
下面的示例
using System;
class Test
{
static void F(params object[] args) {
foreach (object o in args) {
Console.Write(o.GetType().FullName);
Console.Write(" ");
}
Console.WriteLine();
}
static void Main() {
object[] a = {1, "Hello",
123.456};
object o = a;
F(a);
F((object)a);
F(o);
F((object[])o);
}
}
产生输出
System.Int32
System.String System.Double
System.Object[]
System.Object[]
System.Int32 System.String System.Double
在 F 的第一次和最后一次调用中,F 的正常形式是适用的,这是因为存在一个从实参类型到形参类型的转换(这里,两者都是 object[] 类型)。因此,重载决策选择 F 的正常形式,而且将该实参作为常规的值形参传递。在第二次和第三次调用中,F 的正常形式不适用,这是因为不存在从实参类型到形参类型的转换(类型 object 不能隐式转换为类型 object[])。但是,F 的扩展形式是适用的,因此重载决策选择它。因此,这个调用都创建了一个具有单个元素的、类型为 object[] 的数组,并且用给定的实参值(它本身是对一个 object[] 的引用)初始化该数组的唯一元素。
1.6.2 静态方法和实例方法
若一个方法声明中含有 static 修饰符,则称该方法为静态方法。若其中没有 static 修饰符时,则称该方法为实例方法。
静态方法不对特定实例进行操作,在静态方法中引用 this 会导致编译时错误。
实例方法对类的某个给定的实例进行操作,而且可以用 this(第 7.6.7 节)来访问该实例。
在 E.M 形式的 member-access(第 7.6.4 节)中引用一个方法时,如果 M 是静态方法,则 E 必须表示含有 M 的一个类型,而如果 M 是实例方法,则 E 必须表示含有 M 的类型的一个实例。
第 10.3.7 节对静态成员和实例成员之间的差异进行了进一步讨论。
1.6.3 虚方法
若一个实例方法的声明中含有 virtual 修饰符,则称该方法为虚方法。若其中没有 virtual 修饰符,则称该方法为非虚方法。
非虚方法的实现是一成不变的:无论该方法是在声明它的类的实例上调用还是在派生类的实例上调用,实现均相同。与此相反,虚方法的实现可以由派生类取代。取代所继承的虚方法的实现的过程称为重写 (overriding) 该方法(第 10.6.4 节)。
在虚方法调用中,该调用所涉及的那个实例的运行时类型 (run-time type) 确定了要被调用的究竟是该方法的哪一个实现。在非虚方法调用中,相关的实例的编译时类型 (compile-time type) 是决定性因素。准确地说,当在具有编译时类型 C 和运行时类型 R 的实例(其中 R 为 C 或者从 C 派生的类)上用实参列表 A 调用名为 N 的方法时,调用按下述规则处理:
- 首先,将重载决策应用于 C、N 和 A,以从在 C 中声明的和由 C 继承的方法集中选择一个特定的方法 M。第 7.6.5.1 节中对此进行了介绍。
- 然后,如果 M 为非虚方法,则调用 M。
- 否则(M 为虚方法),就会调用就 R 而言 M 的派生程度最大的那个实现。
对于在一个类中声明的或者由类继承的每个虚方法,存在一个就该类而言的方法的派生程度最大的实现 (most derived implementation)。就类 R 而言虚方法 M 的派生度最大的实现按下述规则确定:
- 如果 R 包含 M 的引入 virtual 声明,则这是 M 的派生程度最大的实现。
- 否则,如果 R 包含关于 M 的 override,则这是 M 的派生程度最大的实现。
- 否则,就 R 而言 M 的派生程度最大的实现与就 R 的直接基类而言 M 的派生程度最大的实现相同。
下列实例阐释虚方法和非虚方法之间的区别:
using System;
class A
{
public void F() {
Console.WriteLine("A.F"); }
public virtual void G() {
Console.WriteLine("A.G"); }
}
class B: A
{
new public void F() {
Console.WriteLine("B.F"); }
public override void G() {
Console.WriteLine("B.G"); }
}
class Test
{
static void Main() {
B b = new B();
A a = b;
a.F();
b.F();
a.G();
b.G();
}
}
在该示例中,A 引入一个非虚方法 F 和一个虚方法 G。类 B 引入一个新的非虚方法 F,从而隐藏了继承的 F,并且还重写了继承的方法 G。此例产生输出:
A.F
B.F
B.G
B.G
请注意,语句 a.G() 调用 B.G,而不调用 A.G。这是因为,对调用哪个实际方法实现起决定作用的是该实例的运行时类型(即 B),而不是该实例的编译时类型(即 A)。
由于方法可以隐藏继承来的方法,因此同一个类中可以包含若干个具有相同签名的虚方法。这不会造成多义性问题,因为除派生程度最大的那个方法外,其他方法都被隐藏起来了。在下面的示例中
using System;
class A
{
public virtual void F() {
Console.WriteLine("A.F"); }
}
class B: A
{
public override void F() {
Console.WriteLine("B.F"); }
}
class C: B
{
new public virtual void F() {
Console.WriteLine("C.F"); }
}
class D: C
{
public override void F() {
Console.WriteLine("D.F"); }
}
class Test
{
static void Main() {
D d = new D();
A a = d;
B b = d;
C c = d;
a.F();
b.F();
c.F();
d.F();
}
}
C 类和 D 类包含两个具有相同签名的虚方法:一个是 A 引入的,另一个是 C 引入的。但是,由 C 引入的方法隐藏了从 A 继承的方法。因此,D 中的重写声明所重写的是由 C 引入的方法,D 不可能重写由 A 引入的方法。此例产生输出:
B.F
B.F
D.F
D.F
请注意,通过访问 D 的实例(借助一个派生程度较小的类型,它的方法没有被隐藏起来),可以调用被隐藏的虚方法。
1.6.4 重写方法
若一个实例方法声明中含有 override 修饰符,则称该方法为重写方法 (override method)。重写方法用相同的签名重写所继承的虚方法。虚方法声明用于引入新方法,而重写方法声明则用于使现有的继承虚方法专用化(通过提供该方法的新实现)。
由 override 声明所重写的那个方法称为已重写了的基方法 (overridden base method)。对于在类 C 中声明的重写方法 M,已重写的基方法是通过检查 C 的各个基类类型来确定的,该检查过程如下:从 C 的直接基类类型开始检查,然后依次检查每个后续的直接基类类型,直到在给定的基类类型中至少找到一个在用类型实参替换后与 M 具有相同签名的可访问方法。为了查找已重写了的基方法,可访问方法可以这样来定义:如果一个方法是 public、是 protected、是 protected internal,或者是 internal 并且与 C 声明在同一程序中,则认为它是可访问的。
除非下列所有项对于一个重写声明皆为真,否则将会出现编译时错误:
- 可以按照上面描述的规则找到一个已重写了的基方法。
- 只有一个此类重写的基方法。此限制仅在基类类型是构造类型时(在这种情况下,用类型实参替换会使两个方法的签名相同)才有效。
- 该已重写了的基方法是一个虚的、抽象或重写方法。换句话说,已重写了的基方法不能是静态或非虚方法。
- 已重写了的基方法不是密封方法。
- 重写方法和已重写了的基方法具有相同的返回类型。
- 重写声明和已重写了的基方法具有相同的声明可访问性。换句话说,重写声明不能更改所对应的虚方法的可访问性。但是,如果已重写的基方法是 protected internal,并且声明它的程序集不是包含重写方法的程序集,则重写方法声明的可访问性必须是 protected。
- 重写声明不指定 type-parameter-constraints-clauses,而是从重写的基方法继承约束。请注意,在重写方法中作为类型形参的约束可能会被继承约束中的类型实参替换。在显式指定约束(如值类型或密封类型)时,这可能导致约束不合法。
下面的示例演示重写规则如何对泛型类起作用:
abstract class C<T>
{
public virtual T F() {...}
public virtual C<T> G() {...}
public
virtual void H(C<T> x) {...}
}
class D: C<string>
{
public override string F() {...} // Ok
public override C<string> G() {...} // Ok
public
override void H(C<T> x) {...} //
Error, should be C<string>
}
class E<T,U>: C<U>
{
public override U F() {...} // Ok
public override C<U> G() {...} // Ok
public
override void H(C<T> x) {...} //
Error, should be C<U>
}
重写声明可以使用 base-access(第 7.6.8 节)访问已重写了的基方法。在下面的示例中
class A
{
int x;
public
virtual void PrintFields() {
Console.WriteLine("x =
{0}", x);
}
}
class B: A
{
int y;
public
override void PrintFields() {
base.PrintFields();
Console.WriteLine("y =
{0}", y);
}
}
在 B 中调用 base.PrintFields() 时将调用 A 中声明的 PrintFields 方法。base-access 禁用了虚调用机制,它只是将那个基方法视为非虚方法。如果将 B 中的调用改写为 ((A)this).PrintFields(),它将递归调用在 B 中声明的 PrintFields 方法,而不调用在 A 中声明的该方法,因为 PrintFields 是虚方法,而且 ((A)this) 的运行时类型为 B。
只有在包含了 override 修饰符时,一个方法才能重写另一个方法。在所有其他情况下,声明一个与继承了的方法具有相同签名的方法只会使那个被继承的方法隐藏起来。在下面的示例中
class A
{
public virtual void F() {}
}
class B: A
{
public virtual void F() {} // Warning, hiding inherited F()
}
B 中的 F 方法不包含 override 修饰符,因此不重写 A 中的 F 方法。相反,B 中的 F 方法隐藏 A 中的方法,并且由于该声明中没有包含 new 修饰符,从而会报告一个警告。
在下面的示例中
class A
{
public virtual void F() {}
}
class B: A
{
new private void F() {} // Hides A.F within body of B
}
class C: B
{
public override void F() {} // Ok, overrides A.F
}
B 中的 F 方法将隐藏从 A 中继承的虚 F 方法。由于 B 中的新 F 具有私有访问权限,它的范围只包括 B 的类体而没有延伸到 C。因此,允许 C 中的 F 声明重写从 A 继承的 F。
1.6.5 密封方法
当实例方法声明包含 sealed 修饰符时,称该方法为密封方法 (sealed method)。如果实例方法声明包含 sealed 修饰符,则它必须也包含 override 修饰符。使用 sealed 修饰符可以防止派生类进一步重写该方法。
下面的示例
using System;
class A
{
public virtual void F() {
Console.WriteLine("A.F");
}
public
virtual void G() {
Console.WriteLine("A.G");
}
}
class B: A
{
sealed override public void F() {
Console.WriteLine("B.F");
}
override
public void G() {
Console.WriteLine("B.G");
}
}
class C: B
{
override public void G() {
Console.WriteLine("C.G");
}
}
类 B 提供两个重写方法:一个是带有 sealed 修饰符的 F 方法,另一个是没有 sealed 修饰符的 G 方法。通过使用 sealed modifier,B 就可以防止 C 进一步重写 F。
1.6.6 抽象方法
当实例方法声明包含 abstract 修饰符时,称该方法为抽象方法 (abstract method)。虽然抽象方法同时隐含为虚方法,但是它不能有 virtual 修饰符。
抽象方法声明引入一个新的虚方法,但不提供该方法的实现。相反,非抽象类的派生类需要重写该方法以提供它们自己的实现。由于抽象方法不提供任何实际实现,因此抽象方法的 method-body 只由一个分号组成。
只允许在抽象类(第 10.1.1.1 节)中使用抽象方法声明。
在下面的示例中
public abstract class Shape
{
public abstract void Paint(Graphics g,
Rectangle r);
}
public class Ellipse: Shape
{
public override void Paint(Graphics g,
Rectangle r) {
g.DrawEllipse(r);
}
}
public class Box: Shape
{
public override void Paint(Graphics g,
Rectangle r) {
g.DrawRect(r);
}
}
Shape 类定义了一个可以绘制自身的几何形状对象的抽象概念。Paint 方法是抽象的,这是因为没有有意义的默认实现。Ellipse 和 Box 类是具体的 Shape 实现。由于这些类是非抽象的,因此要求它们重写 Paint 方法并提供实际实现。
如果一个 base-access(第 7.6.8 节)引用的是一个抽象方法,则会导致编译时错误。在下面的示例中
abstract class A
{
public abstract void F();
}
class B: A
{
public override void F() {
base.F(); // Error, base.F is abstract
}
}
调用 base.F() 导致了编译时错误,原因是它引用了抽象方法。
在一个抽象方法声明中可以重写虚方法。这使一个抽象类可以强制从它的派生类重新实现该方法,并使该方法的原始实现不再可用。在下面的示例中
using System;
class A
{
public virtual void F() {
Console.WriteLine("A.F");
}
}
abstract class B: A
{
public abstract override void F();
}
class C: B
{
public override void F() {
Console.WriteLine("C.F");
}
}
类 A 声明一个虚方法,类 B 用一个抽象方法重写此方法,而类 C 重写该抽象方法以提供它自己的实现。
1.6.7 外部方法
当方法声明包含 extern 修饰符时,称该方法为外部方法 (external method)。外部方法是在外部实现的,编程语言通常是使用 C# 以外的语言。由于外部方法声明不提供任何实际实现,因此外部方法的 method-body 只由一个分号组成。外部方法不可以是泛型。
extern 修饰符通常与 DllImport 属性(第 17.5.1 节)一起使用,从而使外部方法可以由 DLL(动态链接库)实现。执行环境可以支持其他用来提供外部方法实现的机制。
当外部方法包含 DllImport 属性时,该方法声明必须同时包含一个 static 修饰符。此示例说明 extern 修饰符和 DllImport 属性的使用:
using
System.Text;
using System.Security.Permissions;
using System.Runtime.InteropServices;
class Path
{
[DllImport("kernel32",
SetLastError=true)]
static extern bool CreateDirectory(string
name, SecurityAttribute sa);
[DllImport("kernel32",
SetLastError=true)]
static extern bool RemoveDirectory(string
name);
[DllImport("kernel32",
SetLastError=true)]
static extern int GetCurrentDirectory(int
bufSize, StringBuilder buf);
[DllImport("kernel32",
SetLastError=true)]
static extern bool
SetCurrentDirectory(string name);
}
1.6.8 分部方法
若一个方法声明中含有 partial 修饰符,则称该方法为分部方法 (partial method)。只能将分部方法声明为分部类型(第 10.2 节)的成员,而且要受多种约束。第 10.2.7 节中对分部方法进行了进一步描述。
1.6.9 扩展方法
当方法的第一个形参包含 this 修饰符时,称该方法为扩展方法 (extension method)。只能在非泛型、非嵌套静态类中声明扩展方法。扩展方法的第一个形参不能带有除 this 之外的其他修饰符,而且形参类型不能是指针类型。
下面是一个声明两个扩展方法的静态类的示例:
public static
class Extensions
{
public static int ToInt32(this string s)
{
return Int32.Parse(s);
}
public static T[] Slice<T>(this T[]
source, int index, int count) {
if (index < 0 || count < 0 ||
source.Length – index <
count)
throw new ArgumentException();
T[] result = new T[count];
Array.Copy(source, index, result, 0,
count);
return result;
}
}
扩展方法是常规静态方法。另外,如果它的包容静态类在范围之内,则可以使用实例方法调用语法(第 7.6.5.2 节)来调用扩展方法,同时将接收器表达式用作第一个实参。
下面的程序使用上面声明的扩展方法:
static class
Program
{
static void Main() {
string[] strings = { "1",
"22", "333", "4444" };
foreach (string s in strings.Slice(1,
2)) {
Console.WriteLine(s.ToInt32());
}
}
}
Slice 方法在 string[] 上可用,ToInt32 方法在字符串上可用,原因是它们都已声明为扩展方法。该程序的含义与下面使用普通静态方法调用的程序相同:
static class
Program
{
static void Main() {
string[] strings = { "1",
"22", "333", "4444" };
foreach (string s in
Extensions.Slice(strings, 1, 2)) {
Console.WriteLine(Extensions.ToInt32(s));
}
}
}
1.6.10 方法体
方法声明中的 method-body 或者由一个 block,或者由一个分号组成。
抽象和外部方法声明不提供方法实现,所以它们的方法体只包含一个分号。对于任何其他方法,方法体是一个块(第 8.2 节),它包含了在调用该方法时应执行的语句。
如果返回类型为 void,或者如果方法是异步方法并且返回类型为 System.Threading.Tasks.Task,则方法的返回类型为 void。否则,非异步方法的结果类型为其返回类型,且返回类型为 System.Threading.Tasks.Task<T> 的异步方法的结果类型为 T。
当方法的结果类型为 void 时,不允许该方法体中的 return 语句(第 8.9.4 节)指定表达式。如果一个 void 方法的方法体的执行正常完成(即控制自方法体的结尾离开),则该方法只是返回到它的当前调用方。
如果方法的返回类型不是 void,则方法体中的每个 return 语句都必须指定一个可隐式转换为结果类型的表达式。对于一个返回值的方法,其方法体的结束点必须是不可到达的。换句话说,在执行返回值的方法中,不允许控制自方法体的结尾离开。
在下面的示例中
class A
{
public int F() {} // Error, return value required
public int G() {
return 1;
}
public int H(bool b) {
if (b) {
return 1;
}
else {
return 0;
}
}
}
返回值的 F 方法导致编译时错误,原因是控制可以超出方法体的结尾。G 和 H 方法是正确的,这是因为所有可能的执行路径都以一个指定返回值的 return 语句结束。
1.6.11 方法重载
第 7.5.2 节对方法重载决策规则进行了描述。
1.7 属性
属性 (property) 是一种用于访问对象或类的特征的成员。属性的示例包括字符串的长度、字体的大小、窗口的标题、客户的名称,等等。属性是字段的自然扩展,此两者都是具有关联类型的命名成员,而且访问字段和属性的语法是相同的。然而,与字段不同,属性不表示存储位置。相反,属性有访问器 (accessor),这些访问器指定在它们的值被读取或写入时需执行的语句。因此属性提供了一种机制,它把读取和写入对象的某些特性与一些操作关联起来;甚至,它们还可以对此类特性进行计算。
属性是使用 property-declaration 声明的:
property-declaration:
attributesopt
property-modifiersopt
type member-name { accessor-declarations }
property-modifiers:
property-modifier
property-modifiers property-modifier
property-modifier:
new
public
protected
internal
private
static
virtual
sealed
override
abstract
extern
member-name:
identifier
interface-type . identifier
property-declaration 可包含一组 attributes(第 17 章)和一个由四个访问修饰符构成的有效组合(第 10.3.5 节),new(第 10.3.4 节)、static(第 10.6.2 节)、virtual(第 10.6.3 节)、override(第 10.6.4 节)、sealed(第 10.6.5 节)、abstract(第 10.6.6 节)和 extern(第 10.6.7 节)。
在有效的修饰符组合方面,属性声明与方法声明(第 10.6 节)遵循相同的规则。
属性声明中的 type 用于指定该声明所引入的属性的类型,而 member-name 则指定该属性的名称。除非该属性是一个显式的接口成员实现,否则 member-name 就只是一个 identifier。对于显式接口成员实现(第 13.4.1 节),member-name 由一个 interface-type 并后接一个“.”和一个 identifier 组成。
属性的 type 必须至少与属性本身(第 3.5.4 节)具有同样的可访问性。
accessor-declarations(必须括在“{”和“}”标记中)声明属性的访问器(第 10.7.2 节)。访问器指定与属性的读取和写入相关联的可执行语句。
虽然访问属性的语法与访问字段的语法相同,但是属性并不归类为变量。因此,不能将属性作为 ref 或 out 参数传递。
属性声明包含 extern 修饰符时,称该属性为外部属性 (external property)。因为外部属性声明不提供任何实际的实现,所以它的每个 accessor-declarations 都仅由一个分号组成。
1.7.1 静态属性和实例属性
当属性声明包含 static 修饰符时,称该属性为静态属性 (static property)。当不存在 static 修饰符时,称该属性为实例属性 (instance property)。
静态属性不与特定实例相关联,因此在静态属性的访问器内引用 this 会导致编译时错误。
实例属性与类的一个给定实例相关联,并且该实例可以在属性的访问器内作为 this(第 7.6.7 节)来访问。
在 E.M 形式的 member-access(第 7.6.4 节)中引用属性时,如果 M 是静态属性,则 E 必须表示包含 M 的类型,如果 M 是实例属性,则 E 必须表示包含 M 的类型的一个实例。
第 10.3.7 节对静态成员和实例成员之间的差异进行了进一步讨论。
1.7.2 访问器
属性的 accessor-declarations 指定与读取和写入该属性相关联的可执行语句。
accessor-declarations:
get-accessor-declaration
set-accessor-declarationopt
set-accessor-declaration
get-accessor-declarationopt
get-accessor-declaration:
attributesopt
accessor-modifieropt get accessor-body
set-accessor-declaration:
attributesopt
accessor-modifieropt set accessor-body
accessor-modifier:
protected
internal
private
protected
internal
internal
protected
accessor-body:
block
;
访问器声明由一个 get-accessor-declaration 或一个 set-accessor-declaration 组成,或者由两者共同组成。每个访问器声明都包含标记 get 或 set,后接可选的 accessor-modifier 和 accessor-body。
accessor-modifier 的使用具有下列限制:
- accessor-modifier 不可用在接口中或显式接口成员实现中。
- 对于没有 override 修饰符的属性或索引器,仅当该属性或索引器同时带有 get 和 set 访问器时,才允许使用 accessor-modifier,并且只能用于其中的一个访问器。
- 对于包含 override 修饰符的属性或索引器,访问器必须匹配被重写的访问器的 accessor-modifier(如果存在)。
- accessor-modifier 声明的可访问性的限制性必须严格高于属性或索引器本身所声明的可访问性。准确地说:
- 如果属性或索引器声明了 public 可访问性,则任何 accessor-modifier 可能为 protected internal、internal、protected 或 private。
- 如果属性或索引器声明了 protected internal 可访问性,则任何 accessor-modifier 可能为 internal、protected 或 private。
- 如果属性或索引器声明了 internal 或 protected 可访问性,则 accessor-modifier 必须为 private。
- 如果属性或索引器声明了 private 可访问性,则任何 accessor-modifier 都不可使用。
对于 abstract 和 extern 属性,每个指定访问器的 accessor-body 只是一个分号。非 abstract、非 extern 属性可以是自动实现的属性 (automatically implemented property),在这种情况下,必须给定 get 访问器和 set 访问器,而且其体均由一个分号组成(第 10.7.3 节)。对于其他任何非 abstract、非 extern 属性的访问器,accessor-body 是一个 block,它指定调用相应访问器时需执行的语句。
get 访问器相当于一个具有属性类型返回值的无形参方法。除了作为赋值的目标,当在表达式中引用属性时,将调用该属性的 get 访问器以计算该属性的值(第 7.1.1 节)。get 访问器体必须符合第 10.6.10 节中所描述的适用于值返回方法的规则。具体而言,get 访问器体中的所有 return 语句都必须指定一个可隐式转换为属性类型的表达式。另外,get 访问器的结束点必须是不可到达的。
set 访问器相当于一个具有单个属性类型值形参和 void 返回类型的方法。set 访问器的隐式参数始终命名为 value。当一个属性作为赋值(第 7.17 节)的目标,或者作为 ++ 或 -- 运算符(第 7.6.9 节、第 7.7.5 节)的操作数被引用时,就会调用 set 访问器,所传递的实参(其值为赋值右边的值或者 ++ 或 -- 运算符的操作数)将提供新值(第 7.17.1 节)。set 访问器体必须符合第 10.6.10 节中所描述的适用于 void 方法的规则。具体而言,不允许 set 访问器体中的 return 语句指定表达式。由于 set 访问器隐式具有名为 value 的形参,因此在 set 访问器中,若在局部变量或常量声明中出现该名称,则会导致编译时错误。
根据 get 和 set 访问器是否存在,属性可按下列规则分类:
- 同时包含 get 访问器和 set 访问器的属性称为读写 (read-write) 属性。
- 只具有 get 访问器的属性称为只读 (read-only) 属性。将只读属性作为赋值目标会导致编译时错误。
- 只具有 set 访问器的属性称为只写 (write-only) 属性。除了作为赋值的目标外,在表达式中引用只写属性是编译时错误。
在下面的示例中
public class Button: Control
{
private string caption;
public
string Caption {
get {
return caption;
}
set {
if (caption != value) {
caption = value;
Repaint();
}
}
}
public
override void Paint(Graphics g, Rectangle r) {
// Painting code goes here
}
}
Button 控件声明了一个公共 Caption 属性。Caption 属性的 get 访问器返回存储在私有 caption 字段中的字符串。set 访问器检查新值是否与当前值不同,如果新值与当前值不同,它将存储新值并重新绘制控件。属性常常跟在上面显示的模式后面:get 访问器只返回一个存储在私有字段中的值,而 set 访问器则用于修改该私有字段,然后执行一些必要的操作,以完全更新所涉及的对象的状态。
下面的示例根据上述 Button 类演示如何使用 Caption 属性:
Button okButton = new Button();
okButton.Caption = "OK"; //
Invokes set accessor
string s = okButton.Caption; //
Invokes get accessor
此处,通过向属性赋值调用 set 访问器,而 get 访问器则通过在表达式中引用该属性来调用。
属性的 get 和 set 访问器都不是独立的成员,也不能单独地声明一个属性的访问器。因此,读写属性的两个访问器不可能具有不同的可访问性。下面的示例
class A
{
private string name;
public
string Name { // Error,
duplicate member name
get { return name; }
}
public
string Name { // Error,
duplicate member name
set { name = value; }
}
}
不是声明单个读写属性。相反,它声明了两个同名的属性,一个是只读的,一个是只写的。由于在同一个类中声明的两个成员不能同名,此示例将导致发生一个编译时错误。
当在一个派生类中用与某个所继承的属性相同的名称声明一个新属性时,该派生属性将会隐藏所继承的属性(同时在读取和写入方面)。在下面的示例中
class A
{
public int P {
set {...}
}
}
class B: A
{
new public int P {
get {...}
}
}
B 中的 P 属性同时在读取和写入方面隐藏 A 中的 P 属性。因此,在下列语句中
B b = new B();
b.P = 1; // Error, B.P is
read-only
((A)b).P = 1; // Ok, reference to A.P
向 b.P 赋值会导致编译时错误,原因是 B 中的只读属性 P 隐藏了 A 中的只写属性 P。但是,仍可以使用强制转换来访问那个被隐藏了的 P 属性。
与公共字段不同,属性在对象的内部状态和它的公共接口之间提供了一种隔离手段。请看此示例:
class Label
{
private int x, y;
private string caption;
public
Label(int x, int y, string caption) {
this.x = x;
this.y = y;
this.caption = caption;
}
public
int X {
get { return x; }
}
public
int Y {
get { return y; }
}
public
Point Location {
get { return new Point(x, y); }
}
public
string Caption {
get { return caption; }
}
}
此处,Label 类将使用两个 int 字段(x 和 y)来存储它的位置。该位置同时采用两种方式公共地公开:X 和 Y 属性,以及类型 Point 的 Location 属性。如果在 Label 的未来版本中采用 Point 结构在内部存储此位置更为方便,则可以不影响类的公共接口就完成更改:
class Label
{
private Point location;
private string caption;
public
Label(int x, int y, string caption) {
this.location = new Point(x, y);
this.caption = caption;
}
public
int X {
get { return location.x; }
}
public
int Y {
get { return location.y; }
}
public
Point Location {
get { return location; }
}
public
string Caption {
get { return caption; }
}
}
相反,如果 x 和 y 是 public readonly 字段,则无法对 Label 类进行上述更改。
通过属性公开状态并不一定比直接公开字段效率低。具体而言,当属性是非虚的且只包含少量代码时,执行环境可能会用访问器的实际代码替换对访问器进行的调用。此过程称为内联 (inlining),它使属性访问与字段访问一样高效,而且仍保留了属性的更高灵活性。
由于调用 get 访问器在概念上等效于读取字段的值,因此使 get 访问器具有可见的副作用被认为是不好的编程风格。在下面的示例中
class Counter
{
private int next;
public
int Next {
get { return next++; }
}
}
Next 属性的值取决于该属性以前被访问的次数。因此,访问此属性会产生可见的副作用,而此属性应当作为一个方法实现。
get 访问器的“无副作用”约定并不意味着 get 访问器应当始终被编写为只返回存储在字段中的值。事实上,get 访问器经常通过访问多个字段或调用方法以计算属性的值。但是,正确设计的 get 访问器不会执行任何导致对象的状态发生可见变化的操作。
属性还可用于将某个资源的初始化延迟到第一次引用该资源时执行。例如:
using System.IO;
public class Console
{
private static TextReader reader;
private static TextWriter writer;
private static TextWriter error;
public
static TextReader In {
get {
if (reader == null) {
reader = new StreamReader(Console.OpenStandardInput());
}
return reader;
}
}
public
static TextWriter Out {
get {
if (writer == null) {
writer = new
StreamWriter(Console.OpenStandardOutput());
}
return writer;
}
}
public
static TextWriter Error {
get {
if (error == null) {
error = new
StreamWriter(Console.OpenStandardError());
}
return error;
}
}
}
Console 类包含三个属性:In、Out 和 Error,它们分别表示三种标准设备:输入、输出和错误设备。通过将这些成员作为属性公开,Console 类可以将它们的初始化延迟到它们被实际使用时。例如,在下列示例中,仅在第一次引用 Out 属性时
Console.Out.WriteLine("hello, world");
才创建输出设备的基础 TextWriter。但是,如果应用程序不引用 In 和 Error 属性,则不会创建这些设备的任何对象。
1.7.3 自动实现的属性
将属性指定为自动实现的属性时,隐藏的后备字段将自动可用于该属性,并实现访问器以执行对该后备字段的读写的操作。
以下示例
public class Point {
public int X { get; set; } //
automatically implemented
public int Y { get; set; } //
automatically implemented
}
等效于下面的声明:
public class
Point {
private int x;
private int y;
public int X { get { return x; } set { x
= value; } }
public int Y { get { return y; } set { y
= value; } }
}
由于支持字段不可访问,因此只能通过属性访问器对其进行读写,即使在包含类型中也是如此。这意味着自动实现的只读或只写属性没有意义,且不允许使用这两种属性。但是,可通过不同的方式为每个访问器设置访问级别。因而,私有后备字段的只读属性的效果可能类似于:
public class
ReadOnlyPoint {
public int X { get; private set; }
public int Y { get; private set; }
public ReadOnlyPoint(int x, int y) { X =
x; Y = y; }
}
此限制还意味着只能使用结构的标准构造函数来实现具有自动实现的属性的结构类型的明确赋值,原因是为该属性本身赋值需要为该结构明确赋值。这意味着用户定义的构造函数必须调用默认构造函数。
1.7.4 可访问性
如果一个访问器带有 accessor-modifier,则该访问器的可访问域(第 3.5.2 节)通过使用 accessor-modifier 所声明的可访问性来确定。如果访问器没有 accessor-modifier,则该访问器的可访问域根据属性或索引器所声明的可访问性来确定。
accessor-modifier 存在与否对于成员查找(第 7.3 节)或重载决策(第 7.5.3 节)毫无影响。属性或索引器的修饰符始终直接确定所绑定到的属性或索引器,并不考虑访问的上下文。
一旦选择了某个特定的属性或索引器,则所涉及的特定访问器的可访问域将用来确定该访问器的使用是否有效:
- 如果作为值(第 7.1.1 节)使用,则 get 访问器必须存在并且可访问。
- 如果作为简单赋值的目标使用(第 7.17.1 节),则 set 访问器必须存在并且可访问。
- 如果作为复合赋值的目标(第 7.17.2 节)或作为 ++ 或 -- 运算符的目标(第 7.5.9 节、第 7.6.5 节)使用,则 get 访问器和 set 访问器都必须存在并且可访问。
在下面的示例中,属性 A.Text 被属性 B.Text 隐藏,甚至是在只调用 set 访问器的上下文中。相比之下,属性 B.Count 对于类 M 不可访问,因此改用可访问的属性 A.Count。
class A
{
public string Text {
get { return "hello"; }
set { }
}
public int Count {
get { return 5; }
set { }
}
}
class B: A
{
private string text =
"goodbye";
private int count = 0;
new public string Text {
get { return text; }
protected set { text = value; }
}
new protected int Count {
get { return count; }
set { count = value; }
}
}
class M
{
static void Main() {
B b = new B();
b.Count = 12; // Calls A.Count set accessor
int i = b.Count; // Calls A.Count get accessor
b.Text = "howdy"; // Error, B.Text set accessor not
accessible
string s = b.Text; // Calls B.Text get accessor
}
}
用来实现接口的访问器不能含有 accessor-modifier。如果仅使用一个访问器实现接口,则另一个访问器可以用 accessor-modifier 声明:
public
interface I
{
string Prop { get; }
}
public class
C: I
{
public Prop {
get { return "April"; } // Must not have a modifier here
internal set {...} // Ok, because I.Prop has no set
accessor
}
}
1.7.5 虚、密封、重写和抽象访问器
virtual 属性声明指定属性的访问器是虚的。virtual 修饰符适用于读写属性的两个访问器(读写属性的访问器不可能只有一个是虚的)。但是,如果其中一个访问器是 private,则将忽略该访问器的 virtual 修饰符,因为它对 virtual 和 private 无意义。
abstract 属性声明指定属性的访问器是虚的,但不提供访问器的实际实现。另外,非抽象派生类还要求通过重写属性以提供它们自己的访问器实现。由于抽象属性声明的访问器不提供实际实现,因此它的 accessor-body 只由一个分号组成。
同时包含 abstract 和 override 修饰符的属性声明表示属性是抽象的并且重写一个基属性。此类属性的访问器也是抽象的。
只能在抽象类(第 10.1.1.1 节)中使用抽象属性声明。通过用一个指定 override 指令的属性声明,可以在派生类中来重写被继承的虚属性的访问器。这称为重写属性声明 (overriding property declaration)。重写属性声明并不声明新属性。相反,它只是对现有虚属性的访问器的实现进行专用化。
重写属性声明必须指定与所继承的属性完全相同的可访问性修饰符、类型和名称。如果被继承的属性只有单个访问器(即该属性是只读或只写的),则重写属性必须只包含该访问器。如果被继承的属性同时包含两个访问器(即该属性是读写的),则重写属性既可以仅包含其中任一个访问器,也可同时包含两个访问器。
重写属性声明可以包含 sealed 修饰符。此修饰符的使用可以防止派生类进一步重写该属性。密封属性的访问器也是密封的。
除了在声明和调用语法中的差异,虚的、密封、重写和抽象访问器与虚的、密封、重写和抽象方法具有完全相同的行为。具体而言,第 10.6.3、10.6.4、10.6.5 和 10.6.6 节中描述的规则都适用,就好像访问器是相应形式的方法一样:
- get 访问器相当于一个无形参方法,该方法具有属性类型的返回值以及与包含属性相同的修饰符。
- set 访问器相当于一个方法,该方法具有单个属性类型的值形参、void 返回类型以及与包含属性相同的修饰符。
在下面的示例中
abstract class A
{
int y;
public
virtual int X {
get { return 0; }
}
public
virtual int Y {
get { return y; }
set { y = value; }
}
public
abstract int Z { get; set; }
}
X 是虚只读属性,Y 是虚读写属性,而 Z 是抽象读写属性。由于 Z 是抽象的,所以包含类 A 也必须声明为抽象的。
下面演示了一个从 A 派生的类:
class B: A
{
int z;
public
override int X {
get { return base.X + 1; }
}
public
override int Y {
set { base.Y = value < 0? 0:
value; }
}
public
override int Z {
get { return z; }
set { z = value; }
}
}
此处,X、Y 和 Z 的声明是重写属性声明。每个属性声明都与它们所继承的属性的可访问性修饰符、类型和名称完全匹配。X 的 get 访问器和 Y 的 set 访问器使用 base 关键字来访问所继承的访问器。Z 的声明重写了两个抽象访问器,因此在 B 中不再有抽象的函数成员,B 也可以是非抽象类。
如果属性声明为 override,则进行重写的代码必须能够访问被重写的访问器。此外,属性或索引器本身以及访问器所声明的可访问性都必须与被重写的成员和访问器所声明的可访问性相匹配。例如:
public class B
{
public virtual int P {
protected set {...}
get {...}
}
}
public class D: B
{
public override int P {
protected set {...} // Must specify protected here
get {...} // Must not have a modifier here
}
}
1.8 事件
事件 (event) 是一种使对象或类能够提供通知的成员。客户端可以通过提供事件处理程序 (event handler) 为相应的事件添加可执行代码。
事件是使用 event-declaration 来声明的:
event-declaration:
attributesopt
event-modifiersopt event type variable-declarators ;
attributesopt event-modifiersopt event type member-name
{ event-accessor-declarations }
event-modifiers:
event-modifier
event-modifiers event-modifier
event-modifier:
new
public
protected
internal
private
static
virtual
sealed
override
abstract
extern
event-accessor-declarations:
add-accessor-declaration remove-accessor-declaration
remove-accessor-declaration
add-accessor-declaration
add-accessor-declaration:
attributesopt add block
remove-accessor-declaration:
attributesopt remove block
event-declaration 可包含一组 attributes(第 17 节)和四个访问修饰符(第 10.3.5 节)的有效组合 new(第 10.3.4 节)、以及 static(第 10.6.2 节)、virtual(第 10.6.3 节)、override(第 10.6.4 节)、sealed(第 10.6.5 节)、abstract(第 10.6.6 节)和 extern(第 10.6.7 节)修饰符。
在有效的修饰符组合方面,事件声明与方法声明(第 10.6 节)遵循相同的规则。
事件声明的 type 必须是 delegate-type(第 4.2 节),而该 delegate-type 必须至少具有与事件本身一样的可访问性(第 3.5.4 节)。
事件声明可能包含 event-accessor-declaration,但如果不包含,则对于非 extern、非 abstract 事件,编译器将自动提供(第 10.8.1 节);对于 extern 事件,访问器由外部提供。
省略了 event-accessor-declaration 的事件声明用于定义一个或多个事件(每个 variable-declarator 各表示一个事件)。特性和修饰符适用于由此类 event-declaration 声明的所有成员。
若 event-declaration 既包含 abstract 修饰符又包含以大括号分隔的 event-accessor-declaration,则会导致编译时错误。
当事件声明包含 extern 修饰符时,称该事件为外部事件 (external event)。因为外部事件声明不提供任何实际的实现,所以在一个外部事件声明中既包含 extern 修饰符又包含 event-accessor-declaration 是错误的。
带 abstract 或 external 修饰符的事件声明的 variable-declarator 包括 variable-initializer 将产生编译时错误。
事件可用作 += 和 -= 运算符(第 7.17.3 节)左边的操作数。这些运算符分别用于将事件处理程序添加到所涉及的事件或从该事件中移除事件处理程序,而该事件的访问修饰符用于控制允许这类运算的上下文。
由于 += 和 -= 是仅有的能够在声明了某个事件的类型的外部对该事件进行的操作,因此,外部代码可以为一个事件添加和移除处理程序,但是不能以其他任何方式来获取或修改基础的事件处理程序列表。
在 x += y 或 x -= y 形式的运算中,如果 x 是一个事件,而且该引用发生在声明了 x 事件的类型之外,则这种运算结果的类型为 void(这正好与该运算的实际效果相反,它用于给 xx 赋值,应该具有 x 所属的类型)。此规则能够禁止外部代码以间接方式来检查一个事件的基础委托。
下面的示例演示如何将事件处理程序添加到 Button 类的实例:
public delegate void EventHandler(object sender, EventArgs e);
public class Button: Control
{
public event EventHandler Click;
}
public class LoginDialog: Form
{
Button OkButton;
Button CancelButton;
public
LoginDialog() {
OkButton = new Button(...);
OkButton.Click += new
EventHandler(OkButtonClick);
CancelButton = new Button(...);
CancelButton.Click += new
EventHandler(CancelButtonClick);
}
void
OkButtonClick(object sender, EventArgs e) {
// Handle OkButton.Click event
}
void
CancelButtonClick(object sender, EventArgs e) {
// Handle CancelButton.Click event
}
}
此处,LoginDialog 实例构造函数将创建两个 Button 实例并将事件处理程序附加到 Click 事件。
1.8.1 类似字段的事件
在包含事件声明的类或结构的程序文本内,某些事件可以像字段一样使用。若要以这种方式使用,事件不能是 abstract 或 extern,而且不能显式包含 event-accessor-declaration。此类事件可以用在任何允许使用字段的上下文中。该字段含有一个委托(第 15 章),它引用已添加到相应事件的事件处理程序列表。如果尚未添加任何事件处理程序,则该字段包含 null。
在下面的示例中
public delegate void EventHandler(object sender, EventArgs e);
public class
Button: Control
{
public event EventHandler Click;
protected void OnClick(EventArgs e) {
if (Click != null) Click(this, e);
}
public void Reset() {
Click = null;
}
}
Click 在 Button 类中用作一个字段。如上例所示,可以在委托调用表达式中检查、修改和使用字段。Button 类中的 OnClick 方法将“引发”Click 事件。“引发一个事件”与“调用一个由该事件表示的委托”这两个概念完全等效,因此没有用于引发事件的特殊语言构造。请注意,在委托调用之前有一个检查,以确保该委托不是 null。
在 Button 类的声明外,Click 成员只能用在 += 和 –= 运算符的左边,如
b.Click += new EventHandler(…);
将一个委托追加到 Click 事件的调用列表,而
b.Click –= new EventHandler(…);
则从 Click 事件的调用列表中移除一个委托。
当编译一个类似字段的事件时,编译器会自动创建一个存储区来存放相关的委托,并为事件创建相应的访问器以向委托字段中添加或移除事件处理程序。添加或移除操作是线程安全的,并且可能会(但不要求)在为实例事件的包含对象加锁(第 8.12 节)的情况下进行,或者在为静态事件的类型对象(第 7.6.10.6 节)加锁的情况下进行。
因此,下列形式的实例事件声明:
class X
{
public event D Ev;
}
将编译为如下语句:
class X
{
private D __Ev; // field to hold the delegate
public event D Ev {
add {
/* add the delegate in a thread
safe way */
}
remove {
/* remove the delegate in a thread
safe way */
}
}
}
在类 X, 中,对 += 和 –= 运算符左侧的 Ev on 的引用将导致调用添加和移除访问器的操作。所有其他对 Ev 的引用将在编译时改为引用隐藏字段 __Ev(第 7.6.4 节)。名称“__Ev”是任意的;隐藏字段可以具有任何名称或根本没有名称。
1.8.2 事件访问器
事件声明通常省略 event-accessor-declaration,如上面的 Button 示例中所示。但会有一些特殊情况,例如,为每个事件设置一个字段所造成的内存开销,有时会变得不可接受。在这种情况下,可以在类中包含 event-accessor-declaration,并采用专用机制来存储事件处理程序列表。
事件的 event-accessor-declarations 指定与添加和移除事件处理程序相关联的可执行语句。
访问器声明由一个 add-accessor-declaration 和一个 remove-accessor-declaration 组成。每个访问器声明都包含标记 add 或 remove,后接一个 block。与 add-accessor-declaration 相关联的 block 指定添加事件处理程序时要执行的语句,而与 remove-accessor-declaration 相关联的 block 指定移除事件处理程序时要执行的语句。
每个 add-accessor-declaration 和 remove-accessor-declaration 相当于一个方法,它具有一个属于事件类型的值形参并且其返回类型为 void。事件访问器的隐式形参名为 value。当事件用在事件赋值中时,就会调用适当的事件访问器。具体而言,如果赋值运算符为 +=,则使用添加访问器,而如果赋值运算符为 -=,则使用移除访问器。在两种情况下,赋值运算符的右操作数都用作事件访问器的实参。add-accessor-declaration 或 remove-accessor-declaration 的块必须符合第 10.6.10 节所描述的用于 void 方法的规则。具体而言,不允许此类块中的 return 语句指定表达式。
由于事件访问器隐式具有一个名为 value 的形参,因此在事件访问器中声明的局部变量或常量若使用该名称,就会导致编译时错误。
在下面的示例中
class Control: Component
{
// Unique keys for events
static readonly object mouseDownEventKey
= new object();
static readonly object mouseUpEventKey =
new object();
//
Return event handler associated with key
protected Delegate GetEventHandler(object
key) {...}
// Add
event handler associated with key
protected void AddEventHandler(object
key, Delegate handler) {...}
//
Remove event handler associated with key
protected void RemoveEventHandler(object
key, Delegate handler) {...}
//
MouseDown event
public event MouseEventHandler MouseDown
{
add {
AddEventHandler(mouseDownEventKey, value); }
remove {
RemoveEventHandler(mouseDownEventKey, value); }
}
//
MouseUp event
public event MouseEventHandler MouseUp {
add { AddEventHandler(mouseUpEventKey,
value); }
remove {
RemoveEventHandler(mouseUpEventKey, value); }
}
//
Invoke the MouseUp event
protected void OnMouseUp(MouseEventArgs
args) {
MouseEventHandler handler;
handler =
(MouseEventHandler)GetEventHandler(mouseUpEventKey);
if (handler != null)
handler(this, args);
}
}
Control 类为事件实现了一个内部存储机制。AddEventHandler 方法将委托值与键关联,GetEventHandler 方法返回当前与键关联的委托,而 RemoveEventHandler 方法将移除一个委托使它不再成为指定事件的一个事件处理程序。可以推断:在这样设计的基础存储机制下,当一个键所关联的委托值为 null 时,不会有存储开销,从而使未处理的事件不占任何存储空间。
1.8.3 静态事件和实例事件
当事件声明包含 static 修饰符时,称该事件为静态事件 (static event)。当不存在 static 修饰符时,称该事件为实例事件 (instance event)。
静态事件不和特定实例关联,因此在静态事件的访问器中引用 this 会导致编译时错误。
实例事件与类的给定实例关联,此实例在该事件的访问器中可以用 this(第 7.6.7 节)来访问。
在 E.M 形式的 member-access(第 7.6.4 节)中引用事件时,如果 M 为静态事件,则 E 必须表示包含 M 的类型,如果 M 为实例事件,则 E 必须表示包含 M 的类型的一个实例。
第 10.3.7 节对静态成员和实例成员之间的差异进行了进一步讨论。
1.8.4 虚、密封、重写和抽象访问器
virtual 事件声明指定事件的访问器是虚的。virtual 修饰符适用于事件的两个访问器。
abstract 事件声明指定事件的访问器是虚的,但是不提供这些访问器的实际实现。而且,非抽象派生类需要通过重写事件来提供它们自己的访问器实现。因为抽象事件声明不提供任何实际的实现,所以它无法提供以大括号界定的 event-accessor-declaration。
同时包含 abstract 和 override 修饰符的事件声明指定该事件是抽象的并重写一个基事件。此类事件的访问器也是抽象的。
只允许在抽象类(第 10.1.1.1 节)中使用抽象事件声明。
继承的虚事件的访问器可以在相关的派生类中用一个指定 override 修饰符的事件声明来进行重写。这称为重写事件声明 (overriding event declaration)。重写事件声明不声明新事件。实际上,它只是专用化了现有虚事件的访问器的实现。
重写事件声明必须采用与被重写事件完全相同的可访问性修饰符、类型和名称。
重写事件声明可以包含 sealed 修饰符。使用此修饰符可以防止相关的派生类进一步重写该事件。密封事件的访问器也是密封的。
重写事件声明包含 new 修饰符会导致编译时错误。
除了在声明和调用语法中的差异,虚的、密封、重写和抽象访问器与虚的、密封、重写和抽象方法具有完全相同的行为。具体而言,第 10.6.3、10.6.4、10.6.5 和 10.6.6 节中描述的规则都适用,就好像访问器是相应形式的方法一样。每个访问器都对应于一个方法,它只有一个属于所涉及的事件类型的值形参、返回类型为 void,且具有与包含事件相同的修饰符。
1.9 索引器
索引器 (indexer) 是这样一个成员:它使对象能够用与数组相同的方式进行索引。索引器是使用 indexer-declaration 来声明的:
indexer-declaration:
attributesopt indexer-modifiersopt indexer-declarator {
accessor-declarations }
indexer-modifiers:
indexer-modifier
indexer-modifiers indexer-modifier
indexer-modifier:
new
public
protected
internal
private
virtual
sealed
override
abstract
extern
indexer-declarator:
type this
[
formal-parameter-list ]
type interface-type .
this
[
formal-parameter-list ]
indexer-declaration 可包含一组 attributes(第 17 节)和一个由四个访问修饰符构成的有效组合(第 10.3.5 节),new (第 10.3.4 节)、virtual(第 10.6.3 节)、override(第 10.6.4 节)、sealed(第 10.6.5 节)、abstract(第 10.6.6 节)和 extern(第 10.6.7 节)。
关于有效的修饰符组合,索引器声明与方法声明(第 10.6 节)遵循相同的规则(唯一的例外是:在索引器声明中不允许使用静态修饰符)。
修饰符 virtual、override 和 abstract 相互排斥,但有一种情况除外。abstract 和 override 修饰符可以一起使用以便抽象索引器可以重写虚索引器。
索引器声明的 type 用于指定由该声明引入的索引器的元素类型。除非索引器是一个显式接口成员的实现,否则该 type 后要跟一个关键字 this。而对于显式接口成员的实现,该 type 后要先跟一个 interface-type、一个“.”,再跟一个关键字 this。与其他成员不同,索引器不具有用户定义的名称。
formal-parameter-list 用于指定索引器的形参。索引器的形参表对应于方法的形参表(第 10.6.1 节),不同之处仅在于索引器的形参表中必须至少含有一个形参,并且不允许使用 ref 和 out 形参修饰符。
索引器的 type 和在 formal-parameter-list 中引用的每个类型都必须至少具有与索引器本身相同的可访问性(第 3.5.4 节)。
accessor-declarations(第 10.7.2 节)(它必须被括在“{”和“}”标记内)用于声明该索引器的访问器。这些访问器用来指定与读取和写入索引器元素相关联的可执行语句。
虽然访问索引器元素的语法与访问数组元素的语法相同,但是索引器元素并不属于变量。因此,不可能将索引器元素作为 ref 或 out 实参传递。
索引器的形参表定义索引器的签名(第 3.6 节)。具体而言,索引器的签名由其形参的数量和类型组成。但索引器元素的类型和形参的名称都不是索引器签名的组成部分。
索引器的签名必须不同于在同一个类中声明的所有其他索引器的签名。
索引器和属性在概念上非常类似,但在下列方面有所区别:
- 属性由它的名称标识,而索引器由它的签名标识。
- 属性是通过 simple-name(第 7.6.2 节)或是 member-access(第 7.6.4 节)来访问的,而索引器元素则是通过 element-access(第 7.6.6.2 节)来访问的。
- 属性可以是 static 成员,而索引器始终是实例成员。
- 属性的 get 访问器对应于不带形参的方法,而索引器的 get 访问器对应于与索引器具有相同的形参表的方法。
- 属性的 set 访问器对应于具有名为 value 的单个形参的方法,而索引器的 set 访问器对应于与索引器具有相同的形参表加上一个名为 value 的附加形参的方法。
- 若在索引器访问器内使用与该索引器的形参相同的名称来声明局部变量,就会导致一个编译时错误。
- 在重写属性声明中,被继承的属性是使用语法 base.P 访问的,其中 P 为属性名称。在重写索引器声明中,被继承的索引器是使用语法 base[E] 访问的,其中 E 是一个用逗号分隔的表达式列表。
除上述差异以外,所有在第 10.7.2 节和第 10.7.3 节中定义的规则都适用于索引器访问器以及属性访问器。
当索引器声明包含 extern 修饰符时,称该索引器为外部索引器 (external indexer)。因为外部索引器声明不提供任何实际的实现,所以它的每个 accessor-declarations 都由一个分号组成。
下面的示例声明了一个 BitArray 类,该类实现了一个索引器,用于访问位数组中的单个位。
using System;
class
BitArray
{
int[] bits;
int length;
public BitArray(int length) {
if (length < 0) throw new
ArgumentException();
bits = new int[((length - 1) >>
5) + 1];
this.length = length;
}
public int Length {
get { return length; }
}
public bool this[int index] {
get {
if (index < 0 || index >=
length) {
throw new
IndexOutOfRangeException();
}
return (bits[index >> 5]
& 1 << index) != 0;
}
set {
if (index < 0 || index >=
length) {
throw new
IndexOutOfRangeException();
}
if (value) {
bits[index >> 5] |= 1
<< index;
}
else {
bits[index >> 5] &=
~(1 << index);
}
}
}
}
BitArray 类的实例所占的内存远少于相应的 bool[](这是由于前者的每个值只占一位,而后者的每个值要占一个字节),而且,它可以执行与 bool[] 相同的操作。
下面的 CountPrimes 类使用 BitArray 和经典的“筛选”算法计算 1 和给定的最大数之间质数的数目:
class
CountPrimes
{
static int Count(int max) {
BitArray flags = new BitArray(max +
1);
int count = 1;
for (int i = 2; i <= max; i++) {
if (!flags[i]) {
for (int j = i * 2; j <=
max; j += i) flags[j] = true;
count++;
}
}
return count;
}
static void Main(string[] args) {
int max = int.Parse(args[0]);
int count = Count(max);
Console.WriteLine("Found {0}
primes between 1 and {1}", count, max);
}
}
请注意,访问 BitArray 的元素的语法与用于 bool[] 的语法完全相同。
下面的示例演示一个具有两个形参的索引器的 26 ´ 10 网格类。第一个形参必须是 A–Z 范围内的大写或小写字母,而第二个形参必须是 0–9 范围内的整数。
using System;
class Grid
{
const int NumRows = 26;
const int NumCols = 10;
int[,] cells = new int[NumRows, NumCols];
public int this[char c, int col] {
get {
c = Char.ToUpper(c);
if (c < 'A' || c > 'Z') {
throw new ArgumentException();
}
if (col < 0 || col >=
NumCols) {
throw new
IndexOutOfRangeException();
}
return cells[c - 'A', col];
}
set {
c = Char.ToUpper(c);
if (c < 'A' || c > 'Z') {
throw new ArgumentException();
}
if (col < 0 || col >=
NumCols) {
throw new
IndexOutOfRangeException();
}
cells[c - 'A', col] = value;
}
}
}
1.9.1 索引器重载
第 7.5.2 节中描述了索引器重载决策规则。
1.10 运算符
运算符 (operator) 是一种用来定义可应用于类实例的表达式运算符的含义的成员。运算符是使用 operator-declaration 来声明的:
operator-declaration:
attributesopt
operator-modifiers
operator-declarator
operator-body
operator-modifiers:
operator-modifier
operator-modifiers operator-modifier
operator-modifier:
public
static
extern
operator-declarator:
unary-operator-declarator
binary-operator-declarator
conversion-operator-declarator
unary-operator-declarator:
type operator
overloadable-unary-operator ( type identifier
)
overloadable-unary-operator: one of
+ - !
~ ++ -- true false
binary-operator-declarator:
type operator
overloadable-binary-operator ( type identifier
, type identifier
)
overloadable-binary-operator:
+
-
*
/
%
&
|
^
<<
right-shift
==
!=
>
<
>=
<=
conversion-operator-declarator:
implicit operator type ( type identifier
)
explicit operator type ( type identifier
)
operator-body:
block
;
有三类可重载运算符:一元运算符(第 10.10.1 节)、二元运算符(第 10.10.2 节)和转换运算符(第 10.10.3 节)。
当运算符声明包含 extern 修饰符时,称该运算符为外部运算符 (external operator)。因为外部运算符不提供任何实际的实现,所以它的 operator-body 由一个分号组成。对于所有其他运算符,operator-body 由一个 block 组成,它指定在调用该运算符时需要执行的语句。运算符的 block 必须遵循第 10.6.10 节中所描述的适用于值返回方法的规则。
下列规则适用于所有的运算符声明:
- 运算符声明必须同时包含一个 public 和一个 static 修饰符。
- 运算符的参数必须是值参数(第 5.1.4 节)。在运算符声明中指定 ref 或 out 形参会导致编译时错误。
- 运算符的签名(第 10.10.1、10.10.2、10.10.3 节)必须不同于在同一个类中声明的所有其他运算符的签名。
- 运算符声明中引用的所有类型都必须具有与运算符本身相同的可访问性(第 3.5.4 节)。
- 同一修饰符在一个运算符声明中多次出现是错误的。
每个运算符类别都有附加的限制,将在下列几节中说明。
与其他成员一样,在基类中声明的运算符由派生类继承。由于运算符声明始终要求声明运算符的类或结构参与运算符的签名,因此在派生类中声明的运算符不可能隐藏在基类中声明的运算符。因此,运算符声明中永远不会要求也不允许使用 new 修饰符。
关于一元和二元运算符的其他信息可以在第 7.3 节中找到。
关于转换运算符的其他信息可以在第 6.4 节中找到。
1.10.1 一元运算符
下列规则适用于一元运算符声明,其中 T 表示包含运算符声明的类或结构的实例类型:
- 一元 +、-、! 或 ~ 运算符必须具有单个 T 或 T? 类型的形参,并且可以返回任何类型。
- 一元 ++ 或 -- 运算符必须带有单个 T 或 T? 类型的形参并且必须返回与它相同或由它派生的类型。
- 一元 true 或 false 运算符必须采用单个 T 或 T? 类型的形参,并且必须返回类型 bool。
一元运算符的签名由运算符标记(+、-、 !、~、++、--、true 或 false)以及单个形参的类型构成。返回类型不是一元运算符的签名的组成部分,形参的名称也不是。
一元运算符 true 和 false 要求成对的声明。如果类只声明了这两个运算符的其中一个而没有声明另一个,将发生编译时错误。第 7.12.2 节和第 7.20 节中对 true 和 false 运算符做了进一步的介绍。
下面的示例演示了对一个整数向量类的 operator ++ 的实现以及随后对它的使用:
public class
IntVector
{
public IntVector(int length) {...}
public int Length {...} // read-only property
public int this[int index] {...} // read-write indexer
public static IntVector operator ++(IntVector
iv) {
IntVector temp = new
IntVector(iv.Length);
for (int i = 0; i < iv.Length;
i++)
temp[i] = iv[i] + 1;
return temp;
}
}
class Test
{
static void Main() {
IntVector iv1 = new IntVector(4); // vector of 4 x 0
IntVector iv2;
iv2 = iv1++; // iv2 contains 4 x 0, iv1 contains 4 x 1
iv2 = ++iv1; // iv2 contains 4 x 2, iv1 contains 4 x 2
}
}
请注意此运算符方法如何返回通过向操作数添加 1 而产生的值,就像后缀增量和减量运算符(第 7.6.9 节)以及前缀增量和减量运算符(第 7.7.5 节)一样。与在 C++ 中不同,此方法并不需要直接修改其操作数的值。实际上,修改操作数的值会违反后缀递增运算符的标准语义。
1.10.2 二元运算符
下列规则适用于二元运算符声明,其中 T 表示包含运算符声明的类或结构的实例类型:
- 二元非移位运算符必须带有两个形参,其中至少有一个必须为类型 T 或 T?,并且可返回其中的任一类型。
- 二元 << 或 >> 运算符必须带有两个形参,其中第一个必须具有类型 T 或 T?,第二个必须具有类型 int 或 int?,并且可返回其中的任一类型。
二元运算符的签名由运算符标记(+、-、*、/、%、&、|、^、<<、>>、==、!=、>、<、>= 或 <=)以及两个形参的类型构成。它本身的返回类型及形参的名称不是二元运算符签名的组成部分。
某些二元运算符要求成对地声明。对于要求成对地声明的运算符,若声明了其中一个,就必须对另一个作出相匹配的声明。如果返回类型 R1 和 R2 之间、操作数类型 P1 和 P2 之间、操作数类型 Q1 和 Q2 之间存在标识转换,则两个运算符声明 R1 op1(P1, Q1) 和 R2 op2(P2, Q2) 匹配。下列运算符要求成对的声明:
- operator == 和 operator !=
- operator > 和 operator <
- operator >= 和 operator <=
1.10.3 转换运算符
转换运算符声明引入用户定义的转换 (user-defined conversion)(第 6.4 节),此转换可以扩充预定义的隐式和显式转换。
包含 implicit 关键字的转换运算符声明引入用户定义的隐式转换。隐式转换可以在多种情况下发生,包括函数成员调用、强制转换表达式和赋值。第 6.1 节对此有进一步描述。
包含 explicit 关键字的转换运算符声明引入用户定义的显式转换。显式转换可以发生在强制转换表达式中,第 6.2 节中对此进行了进一步描述。
转换运算符将某个源类型(由该转换运算符的形参类型指定)转换为目标类型(由该转换运算符的返回类型指定)。
对于给定的源类型 S 和目标类型 T,如果 S 或 T 是可以为 null 的类型,则让 S0 和 T0 引用它们的基础类型,否则 S0 和 T0 分别等于 S 和 T。仅当以下条件皆为真时,才允许类或结构声明从源类型 S 到目标类型 T 的转换:
- S0 和 T0 是不同的类型。
- S0 和 T0 中有一个是声明该运算符的类类型或结构类型。
- S0 和 T0 都不是 interface-type。
- 除用户定义的转换之外,不存在从 S 到 T 或从 T 到 S 的转换。
为了实现这些规则,将任何与 S 或 T 关联的类型形参都视为与其他类型没有继承关系的特有类型,而且忽略对这些类型形参的任何约束。
在下面的示例中
class C<T> {...}
class
D<T>: C<T>
{
public static implicit operator
C<int>(D<T> value) {...} //
Ok
public static implicit operator C<string>(D<T> value) {...} // Ok
public static implicit operator
C<T>(D<T> value) {...} //
Error
}
前两个运算符声明是允许的,因为根据第 10.9.3 节,T 和 int 以及 string 分别被视为没有关系的唯一类型。但是,第三个运算符是错误的,因为 C<T> 是 D<T> 的基类。
从第二条规则可以推知,转换运算符必须将声明了该运算符的类或结构类型或者作为目标类型,或者作为源类型。例如,一个类或结构类型 C 可以定义从 C 到 int 和从 int 到 C 的转换,但不能定义从 int 到 bool 的转换。
不能直接重新定义一个已存在的预定义转换。因此,不允许转换运算符将 object 转换为其他类型或将其他类型转换为 object,这是因为已存在隐式和显式转换来执行 object 与所有其他类型之间的转换。同样,转换的源类型和目标类型不能是对方的基类型,这是由于已经存在这样的转换。
但是,对于特定类型实参,可以在泛型类型上声明这样的运算符,即这些运算符指定了已经作为预定义转换而存在的转换。在下面的示例中
struct
Convertible<T>
{
public static implicit operator
Convertible<T>(T value) {...}
public static explicit operator
T(Convertible<T> value) {...}
}
当把类型 object 指定为 T 的类型实参时,第二个运算符将声明一个已经存在的转换(存在从任何类型到类型 object 的隐式转换,因此也存在显式转换)。
在两个类型之间存在预定义转换的情况下,这些类型之间的任何用户定义的转换将被忽略。具体包括:
- 如果存在从类型S 到类型 T 的预定义隐式转换(第 6.1 节),则从 S 到 T 的所有用户定义的转换(隐式或显式)将被忽略。
- 如果存在从类型 S 到类型 T 的预定义显式转换(第 6.2 节),则从 S 到 T 的所有用户定义的显式转换将被忽略。此外:
- 如果 T 是接口类型,则会忽略从 S 到 T 的用户定义的隐式转换。
- 否则,仍会考虑从 S 到 T 的用户定义的隐式转换。
对于除 object 以外的所有类型,上面的 Convertible<T> 类型声明的运算符都不会与预定义的转换发生冲突。例如:
void F(int
i, Convertible<int> n) {
i = n; //
Error
i = (int)n; // User-defined explicit conversion
n = i; //
User-defined implicit conversion
n = (Convertible<int>)i; // User-defined implicit conversion
}
但是对于类型 object,除了下面这个特例之外,预定义的转换将在其他所有情况下隐藏用户定义的转换:
void
F(object o, Convertible<object> n) {
o = n; //
Pre-defined boxing conversion
o = (object)n; // Pre-defined boxing conversion
n = o; //
User-defined implicit conversion
n = (Convertible<object>)o; // Pre-defined unboxing conversion
}
不允许使用用户定义的转换从 interface-type 进行转换或转换为 interface-type。具体而言,此限制确保了在转换为 interface-type 时不会发生任何用户定义的转换,以及只有在要转换的对象实际上实现了指定的 interface-type 时,到该 interface-type 的转换才会成功。
转换运算符的签名由源类型和目标类型组成。(请注意,这是唯一一种其返回类型参与签名的成员形式。)转换运算符的 implicit 或 explicit 类别不是运算符签名的组成部分。因此,类或结构不能同时声明具有相同源类型和目标类型的 implicit 和 explicit 转换运算符。
一般来说,如果设计一个用户定义的隐式转换,就应当确保执行该转换时决不会引发异常,并且也决不会丢失信息。如果用户定义的转换可能导致引发异常(例如,由于源实参超出范围)或丢失信息(如放弃高序位),则该转换应该定义为显式转换。
在下面的示例中
using System;
public
struct Digit
{
byte value;
public Digit(byte value) {
if (value < 0 || value > 9)
throw new ArgumentException();
this.value = value;
}
public static implicit operator byte(Digit d) {
return d.value;
}
public static explicit operator Digit(byte b) {
return new Digit(b);
}
}
从 Digit 到 byte 的转换是隐式的,这是因为它永远不会引发异常或丢失信息,但是从 byte 到 Digit 的转换是显式的,这是因为 Digit 只能表示 byte 的可能值的一个子集。
1.11 实例构造函数
实例构造函数 (instance constructor) 是实现初始化类实例所需操作的成员。实例构造函数是使用 constructor-declaration 来声明的:
constructor-declaration:
attributesopt
constructor-modifiersopt
constructor-declarator
constructor-body
constructor-modifiers:
constructor-modifier
constructor-modifiers
constructor-modifier
constructor-modifier:
public
protected
internal
private
extern
constructor-declarator:
identifier (
formal-parameter-listopt
)
constructor-initializeropt
constructor-initializer:
: base (
argument-listopt )
: this
(
argument-listopt )
constructor-body:
block
;
constructor-declaration 可以包含一组 attributes(第 17 章)、四个访问修饰符(第 10.3.5 节)的有效组合和一个 extern(第 10.6.7 节)修饰符。一个构造函数声明中同一修饰符不能多次出现。
constructor-declarator 中的 identifier 必须是声明了该实例构造函数的那个类的名称。如果指定了任何其他名称,则发生编译时错误。
可选的实例构造函数的 formal-parameter-list 必须遵循与方法的 formal-parameter-list(第 10.6 节)同样的规则。此形参表定义实例构造函数的签名(第 3.6 节),并且在调用中控制重载决策(第 7.5.2 节)过程以选择某个特定实例的构造函数。
在实例构造函数的 formal-parameter-list 中引用的各个类型必须至少具有与构造函数本身相同的可访问性(第 3.5.4 节)。
可选的 constructor-initializer 用于指定在执行此实例构造函数的 constructor-body 中给出的语句之前需要调用的另一个实例构造函数。第 10.11.1 节对此有进一步描述。
当构造函数声明中包含 extern 修饰符时,称该构造函数为外部构造函数 (external constructor)。因为外部构造函数声明不提供任何实际的实现,所以它的 constructor-body 仅由一个分号组成。对于所有其他构造函数,constructor-body 都由一个 block 组成,它用于指定初始化该类的一个新实例时需要执行的语句。这正好相当于一个具有 void 返回类型的实例方法的 block(第 10.6.10 节)。
实例构造函数是不能继承的。因此,一个类除了自已声明的实例构造函数外,不可能有其他的实例构造函数。如果一个类不包含任何实例构造函数声明,则会自动地为该类提供一个默认实例构造函数(第 10.11.4 节)。
实例构造函数是由 object-creation-expression(第 7.6.10.1 节)并通过 constructor-initializer 调用的。
1.11.1 构造函数初始值设定项
除了类 object 的实例构造函数以外,所有其他的实例构造函数都隐式地包含一个对另一个实例构造函数的调用,该调用紧靠在 constructor-body 的前面。要隐式调用的构造函数是由 constructor-initializer 确定的:
- base(argument-listopt) 形式的实例构造函数初始值设定项导致调用直接基类中的实例构造函数。该构造函数是根据 argument-list 和第 7.5.3 节中的重载决策规则选择的。候选实例构造函数集由直接基类中包含的所有可访问的实例构造函数组成,如果直接基类中未声明任何实例构造函数,则候选实例构造函数集由默认构造函数(第 10.11.4 节)组成。如果此集为空,或者无法标识单个最佳实例构造函数,就会发生编译时错误。
- this(argument-listopt) 形式的实例构造函数初始值设定项导致调用该类本身所声明的实例构造函数。该构造函数是根据 argument-list 和第 7.5.3 节中的重载决策规则选择的。候选实例构造函数集由类本身声明的所有可访问的实例构造函数组成。如果此集为空,或者无法标识单个最佳实例构造函数,就会发生编译时错误。如果实例构造函数声明中包含调用构造函数本身的构造函数初始值设定项,则发生编译时错误。
如果一个实例构造函数中没有构造函数初始值设定项,将会隐式地添加一个 base() 形式的构造函数初始值设定项。因此,下列形式的实例构造函数声明
C(...) {...}
完全等效于
C(...): base() {...}
实例构造函数声明中的 formal-parameter-list 所给出的形参范围包含该声明的实例构造函数初始值设定项。因此,构造函数初始值设定项可以访问该构造函数的形参。例如:
class A
{
public A(int x, int y) {}
}
class B: A
{
public B(int x, int y): base(x + y, x -
y) {}
}
实例构造函数初始值设定项不能访问正在创建的实例。因此在构造函数初始值设定项的实参表达式中引用 this 属于编译时错误,就像实参表达式通过 simple-name 引用任何实例成员属于编译时错误一样。
1.11.2 实例变量初始值设定项
当实例构造函数没有构造函数初始值设定项时,或仅具有 base(...) 形式的构造函数初始值设定项时,该构造函数就会隐式地执行在该类中声明的实例字段的初始化操作,这些操作由对应的字段声明中的 variable-initializer 指定。这对应于一个赋值序列,它们会在进入构造函数时,在对直接基类的构造函数进行隐式调用之前立即执行。这些变量初始值设定项按它们出现在类声明中的文本顺序执行。
1.11.3 构造函数执行
变量初始值设定项被转换为赋值语句,而这些语句将在对基类实例构造函数进行调用之前执行。这种排序确保了在执行任何访问该实例的语句之前,所有实例字段都已按照它们的变量初始值设定项进行了初始化。
给定示例
using System;
class A
{
public A() {
PrintFields();
}
public virtual void PrintFields() {}
}
class B: A
{
int x = 1;
int y;
public B() {
y = -1;
}
public override void PrintFields() {
Console.WriteLine("x = {0}, y =
{1}", x, y);
}
}
当使用 new B() 创建 B 的实例时,产生如下输出:
x = 1, y = 0
x 的值为 1,这是由于变量初始值设定项是在调用基类实例构造函数之前执行的。但是,y 的值为 0(int 型变量的默认值),这是因为对 y 的赋值直到基类构造函数返回之后才执行。
可以这样设想来帮助理解:将实例变量初始值设定项和构造函数初始值设定项视为自动插入到 constructor-body 之前的语句。下面的示例
using System;
using System.Collections;
class A
{
int x = 1, y = -1, count;
public A() {
count = 0;
}
public A(int n) {
count = n;
}
}
class B: A
{
double sqrt2 = Math.Sqrt(2.0);
ArrayList items = new ArrayList(100);
int max;
public B(): this(100) {
items.Add("default");
}
public B(int n): base(n – 1) {
max = n;
}
}
包含若干个变量初始值设定项,还包含两种形式(base 和 this)的构造函数初始值设定项。此示例对应于下面演示的代码,其中每个注释指示一个自动插入的语句(用于自动插入的构造函数调用的语法是无效的,而只是用来阐释此机制)。
using System.Collections;
class A
{
int x, y, count;
public A() {
x = 1; // Variable initializer
y = -1; // Variable initializer
object(); // Invoke object() constructor
count = 0;
}
public A(int n) {
x = 1; // Variable initializer
y = -1; // Variable initializer
object(); // Invoke object() constructor
count = n;
}
}
class B: A
{
double sqrt2;
ArrayList items;
int max;
public B(): this(100) {
B(100); // Invoke B(int) constructor
items.Add("default");
}
public B(int n): base(n – 1) {
sqrt2 = Math.Sqrt(2.0); // Variable initializer
items = new ArrayList(100); // Variable initializer
A(n – 1); //
Invoke A(int) constructor
max = n;
}
}
1.11.4 默认构造函数
如果一个类不包含任何实例构造函数声明,则会自动地为该类提供一个默认实例构造函数。默认构造函数只是调用直接基类的无形参构造函数。对于一个抽象类,它的默认构造函数的声明可访问性是受保护的。而对于非抽象类,它的默认构造函数的声明可访问性是公共的。因此,默认构造函数始终为下列形式:
protected C(): base() {}
或
public C(): base() {}
其中 C 为类的名称。如果重载决策无法确定基类构造函数初始值设定项的唯一最佳候选,则产生编译时错误。
在下面的示例中
class Message
{
object sender;
string text;
}
由于类不包含任何实例构造函数声明,因此就为它提供了一个默认构造函数。因而,此示例完全等效于
class Message
{
object sender;
string text;
public Message(): base() {}
}
1.11.5 私有构造函数
当类 T 只声明了私有实例构造函数时,则在 T 的程序文本外部,既不可能从 T 派生出新的类,也不可能直接创建 T 的任何实例。因此,如果欲设计一个类,它只包含静态成员而且有意使它不能被实例化,则只需给它添加一个空的私有实例构造函数,即可达到目的。例如:
public class
Trig
{
private Trig() {} // Prevent instantiation
public const double PI = 3.14159265358979323846;
public static double Sin(double x) {...}
public static double Cos(double x) {...}
public static double Tan(double x) {...}
}
Trig 类用于将相关的方法和常量组合在一起,但是它不能被实例化。因此它声明了单个空的私有实例构造函数。若要取消默认构造函数的自动生成,必须至少声明一个实例构造函数。
1.11.6 可选的实例构造函数形参
this(...) 形式的构造函数初始值设定项通常与重载一起使用,以实现可选的实例构造函数形参。在下面的示例中
class Text
{
public Text(): this(0, 0, null) {}
public Text(int x, int y): this(x, y, null) {}
public Text(int x, int y, string s) {
// Actual constructor implementation
}
}
前两个实例构造函数只为调用中没有传递过来的实参提供相应的默认值。这两个构造函数都使用 this(...) 构造函数初始值设定项来调用实际完成初始化新实例工作的第三个实例构造函数。这样,实际效果就是该实例构造函数具有可选的形参:
Text t1 = new
Text(); // Same as Text(0,
0, null)
Text t2 = new Text(5, 10); //
Same as Text(5, 10, null)
Text t3 = new Text(5, 20, "Hello");
1.12 静态构造函数
静态构造函数 (static constructor) 是一种用于实现初始化封闭式类类型所需操作的成员。静态构造函数是使用 static-constructor-declaration 来声明的:
static-constructor-declaration:
attributesopt
static-constructor-modifiers
identifier ( )
static-constructor-body
static-constructor-modifiers:
externopt static
static
externopt
static-constructor-body:
block
;
static-constructor-declaration 可包含一组 attributes(第 17 章)和一个 extern 修饰符(第 10.6.7 节)。
static-constructor-declaration 的 identifier 必须是声明了该静态构造函数的那个类的名称。如果指定了任何其他名称,则发生编译时错误。
当静态构造函数声明包含 extern 修饰符时,称该静态构造函数为外部静态构造函数 (external static constructor)。因为外部静态构造函数声明不提供任何实际的实现,所以它的 static-constructor-body 由一个分号组成。对于所有其他的静态构造函数声明,static-constructor-body 都是一个 block,它指定当初始化该类时需要执行的语句。这正好相当于具有 void 返回类型的静态方法的 method-body(第 10.6.10 节)。
静态构造函数是不可继承的,而且不能被直接调用。
封闭式类类型的静态构造函数在给定应用程序域中至多执行一次。应用程序域中第一次发生以下事件时将触发静态构造函数的执行:
- 创建类类型的实例。
- 引用类类型的任何静态成员。
如果类中包含用来开始执行的 Main 方法(第 3.1 节),则该类的静态构造函数将在调用 Main 方法之前执行。
若要初始化新的封闭式类类型,需要先为该特定的封闭类型创建一组新的静态字段(第 10.5.1 节)。将其中的每个静态字段初始化为默认值(第 5.2 节)。下一步,为这些静态字段执行静态字段初始值设定项(第 10.4.5.1 节)。最后,执行静态构造函数。
下面的示例
using System;
class Test
{
static void Main() {
A.F();
B.F();
}
}
class A
{
static A() {
Console.WriteLine("Init
A");
}
public static void F() {
Console.WriteLine("A.F");
}
}
class B
{
static B() {
Console.WriteLine("Init
B");
}
public static void F() {
Console.WriteLine("B.F");
}
}
一定产生输出:
Init A
A.F
Init B
B.F
因为 A 的静态构造函数的执行是通过调用 A.F 触发的,而 B 的静态构造函数的执行是通过调用 B.F 触发的。
上述过程有可能构造出循环依赖关系,其中,带有变量初始值设定项的静态字段能够在其处于默认值状态时被观测。
下面的示例
using System;
class A
{
public static int X;
static A() {
X = B.Y + 1;
}
}
class B
{
public static int Y = A.X + 1;
static B() {}
static void Main() {
Console.WriteLine("X = {0}, Y =
{1}", A.X, B.Y);
}
}
产生输出
X = 1, Y = 2
要执行 Main 方法,系统在运行类 B 的静态构造函数之前首先要运行 B.Y 的初始值设定项。因为引用了 A.X 的值,所以 Y 的初始值设定项导致运行A 的静态构造函数。这样,A 的静态构造函数将继续计算 X 的值,从而获取 Y 的默认值 0,而 A.X 被初始化为 1。这样就完成了运行 A 的静态字段初始值设定项和静态构造函数的进程,控制返回到 Y 的初始值的计算,计算结果变为 2。
由于静态构造函数只为每个封闭构造类类型执行一次,因此对于无法通过约束(第 10.1.5 节)在编译时进行检查的类型形参来说,此处是强制进行运行时检查的方便位置。例如,下面的类型使用静态构造函数检查类型实参是否为一个枚举:
class Gen<T> where T: struct
{
static Gen() {
if (!typeof(T).IsEnum) {
throw new
ArgumentException("T must be an enum");
}
}
}
1.13 析构函数
析构函数 (destructor) 是一种用于实现销毁类实例所需操作的成员。析构函数是用 destructor-declaration 来声明的:
destructor-declaration:
attributesopt externopt ~ identifier (
) destructor-body
destructor-body:
block
;
destructor-declaration 可以包括一组 attributes(第 17 章)。
destructor-declarator 的 identifier 必须就是声明了该析构函数的那个类的名称。如果指定了任何其他名称,则发生编译时错误。
当析构函数声明包含 extern 修饰符时,称该析构函数为外部析构函数 (external destructor)。因为外部析构函数声明不提供任何实际的实现,所以它的 destructor-body 由一个分号组成。对于所有其他析构函数,destructor-body 都由一个 block 组成,它指定当销毁该类的一个实例时需要执行的语句。destructor-body 正好对应于具有 void 返回类型(第 10.6.10 节)的实例方法的 method-body。
析构函数是不可继承的。因此,除了自已所声明的析构函数外,一个类不具有其他析构函数。
由于析构函数要求不能带有形参,因此它不能被重载,所以一个类至多只能有一个析构函数。
析构函数是自动调用的,它不能被显式调用。当任何代码都不再可能使用一个实例时,该实例就符合被销毁的条件。此后,它所对应的实例析构函数随时均可能被调用。销毁一个实例时,按照从派生程度最大到派生程度最小的顺序,调用该实例的继承链中的各个析构函数。析构函数可以在任何线程上执行。有关控制何时及如何执行析构函数的规则的进一步讨论,请参见第 3.9 节。
下列示例的输出
using System;
class A
{
~A() {
Console.WriteLine("A's
destructor");
}
}
class B: A
{
~B() {
Console.WriteLine("B's
destructor");
}
}
class Test
{
static void Main() {
B b = new B();
b = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
为
B’s
destructor
A’s destructor
这是由于继承链中的析构函数是按照从派生程度最大到派生程度最小的顺序调用的。
析构函数是通过重写 System.Object 中的虚方法 Finalize 实现的。C# 程序中不允许重写此方法或直接调用它(或它的重写)。例如,下列程序
class A
{
override protected void Finalize() {} // error
public void F() {
this.Finalize(); // error
}
}
包含两个错误。
编译器的行为就像此方法和它的重写根本不存在一样。因此,以下程序:
class A
{
void Finalize() {} // permitted
}
是有效的,所显示的方法隐藏了 System.Object 的 Finalize 方法。
有关从析构函数引发异常时的行为的讨论,请参见第 16.3 节。
1.14 迭代器
使用迭代器块(第 7.5 节)实现的函数成员(第 8.2 节)称为迭代器 (iterator)。
只要相应函数成员的返回类型是枚举器接口(第 10.14.1 节)之一或可枚举接口(第 10.14.2 节)之一,迭代器块就可用作该函数成员的函数体。它可以作为 method-body、operator-body 或 accessor-body 出现,而不能将事件、实例构造函数、静态构造函数和析构函数作为迭代器来实现。
当使用迭代器块实现函数成员时,为该函数成员的形参列表指定任何 ref 或 out 形参将产生编译时错误。
1.14.1 枚举器接口
枚举器接口 (enumerator interface) 为非泛型接口 System.Collections.IEnumerator 和泛型接口 System.Collections.Generic.IEnumerator<T> 的所有实例化。为简洁起见,本章中将这些接口分别表示为 IEnumerator 和 IEnumerator<T>。
1.14.2 可枚举接口
可枚举接口 (enumerable interface) 为非泛型接口 System.Collections.IEnumerable 和泛型接口 System.Collections.Generic.IEnumerable<T> 的所有实例化。为简洁起见,本章中将这些接口分别表示为 IEnumerable 和 IEnumerable<T>。
1.14.3 产生类型
迭代器产生一系列值,所有值的类型均相同。此类型称为迭代器的产生类型 (yield type)。
- 返回 IEnumerator 或 IEnumerable 的迭代器的产生类型是 object。
- 返回 IEnumerator<T> 或 IEnumerable<T> 的迭代器的产生类型是 T。
1.14.4 枚举器对象
如果返回枚举器接口类型的函数成员是使用迭代器块实现的,调用该函数成员不会立即执行迭代器块中的代码。而是先创建并返回一个枚举器对象 (enumerator object)。此对象封装了在迭代器块中指定的代码,并且在调用该枚举器对象的 MoveNext 方法时执行该迭代器块中的代码。枚举器对象具有下列特点:
- 它实现了 IEnumerator 和 IEnumerator<T>,其中 T 为迭代器的产生类型。
- 它实现了 System.IDisposable。
- 它以传递给该函数成员的实参值(如果存在)和实例值的副本进行初始化。
- 它有四种可能的状态:运行前 (before)、运行中 (running)、挂起 (suspended) 和运行后 (after),并且初始状态为运行前 (before) 状态。
枚举器对象通常是编译器生成的枚举器类的一个实例,它封装了迭代器块中的代码,并实现了枚举器接口,但也可能实现其他方法。如果枚举器类由编译器生成,则该类将直接或间接嵌套在包含该函数成员的类中,它将具有私有可访问性,并且它将具有一个供编译器使用的保留名称(第 2.4.2 节)。
枚举器对象可实现除上面指定的那些接口以外的其他接口。
下面的各节将描述由枚举器对象所提供的 IEnumerable 和 IEnumerable<T> 接口实现的 MoveNext、Current 和 Dispose 成员的确切行为。
请注意,枚举器对象不支持 IEnumerator.Reset 方法。调用此方法将导致引发 System.NotSupportedException。
1.14.4.1 MoveNext 方法
枚举器对象的 MoveNext 方法封装了迭代器块的代码。调用 MoveNext 方法将执行迭代器块中的代码,并相应设置枚举器对象的 Current 属性。MoveNext 执行的具体操作取决于调用 MoveNext 时的枚举器对象的状态:
- 如果枚举器对象的状态为运行前 (before),则调用 MoveNext 会:
- 将状态更改为运行中 (running)。
- 将迭代器块的形参(包括 this)初始化为实参值以及初始化该枚举器对象时所保存的实例值。
- 从头开始执行迭代器块,直到执行被中断(如后文所述)。
- 如果枚举器对象的状态为运行中 (running),则调用 MoveNext 的结果不确定。
- 如果枚举器对象的状态为挂起 (suspended),则调用 MoveNext 将:
- 将状态更改为运行中 (running)。
- 将所有局部变量和形参(包括 this)的值恢复为迭代器块的执行上次挂起时保存的值。注意,这些变量所引用对象的内容可能自上次调用 MoveNext 之后已经发生更改。
- 恢复执行紧跟在引起执行挂起的 yield return 语句后面的迭代器块,并一直继续,直到执行中断(如后文所述)。
- 如果枚举器对象的状态为运行后 (after),则调用 MoveNext 将返回 false。
当 MoveNext 执行迭代器块时,可以采用四种方式来中断执行:通过 yield return 语句、通过 yield break 语句、到达迭代器块的末尾以及引发异常并将异常传播到迭代器块之外。
- 当遇到 yield return 语句时(第 8.14 节):
- 计算该语句中给出的表达式,隐式转换为产生类型,并赋给枚举器对象的 Current 属性。
- 迭代器体的执行被挂起。所有局部变量和形参(包括 this)的值被保存,此 yield return 语句的位置也被保存。如果 yield return 语句在一个或多个 try 块内,则此时与之关联的 finally 块将不会执行。
- 枚举器对象的状态更改为挂起 (suspended)。
- MoveNext 方法向其调用方返回 true,指示迭代成功前进至下一个值。
- 当遇到 yield break 语句时(第 8.14 节):
- 如果 yield break 语句在一个或多个 try 块内,则与之关联的 finally 块将执行。
- 枚举器对象的状态更改为运行后 (after)。
- MoveNext 方法向其调用方返回 false,指示迭代完成。
- 当遇到迭代器体的结束处时:
- 枚举器对象的状态更改为运行后 (after)。
- MoveNext 方法向其调用方返回 false,指示迭代完成。
- 当引发异常并传播到迭代器块之外时:
- 通过异常传播机制执行迭代器体内的相应 finally 块。
- 枚举器对象的状态更改为运行后 (after)。
- 异常继续传播至 MoveNext 方法的调用方。
1.14.4.2 Current 属性
枚举器对象的 Current 属性将受迭代器块中的 yield return 语句影响。
当枚举器对象处于挂起 (suspended) 状态时,Current 的值为上一次调用 MoveNext 时设置的值。当枚举器对象处于运行前 (before)、运行中 (running) 或运行后 (after) 状态时,访问 Current 的结果不确定。
对于产生类型不是 object 的迭代器,通过枚举器对象的 IEnumerable 实现来访问 Current 的结果对应于通过枚举器对象的 IEnumerator<T> 实现来访问 Current 并将该结果强制转换为 object。
1.14.4.3 Dispose 方法
Dispose 方法用于通过使枚举器对象变为运行后 (after) 状态来清除迭代。
- 如果枚举器对象的状态为运行前 (before),则调用 Dispose 将把状态更改为运行后 (after)。
- 如果枚举器对象的状态为运行中 (running),则调用 Dispose 的结果不确定。
- 如果枚举器对象的状态为挂起 (suspended),则调用 Dispose 将:
- 将状态更改为运行中 (running)。
- 执行所有 finally 块,就像最后执行的 yield return 语句是 yield break 语句一样。如果这导致引发异常,并且异常传播到迭代器体之外,则枚举器对象的状态设置为运行后 (after),并且将异常传播到 Dispose 方法的调用方。
- 将状态更改为运行后 (after)。
- 如果枚举器对象的状态为运行后 (after),则调用 Dispose 没有任何作用。
1.14.5 可枚举对象
如果返回可枚举接口类型的函数成员是使用迭代器块实现的,调用该函数成员不会立即执行迭代器块中的代码。而是先创建并返回一个可枚举对象 (enumerable object)。可枚举对象的 GetEnumerator 方法返回一个封装有迭代器块中指定的代码的枚举器对象,当调用该枚举器对象的 MoveNext 方法时,将执行迭代器块中的代码。可枚举对象具有下列特点:
- 它实现了 IEnumerable 和 IEnumerable<T>,其中 T 为迭代器的产生类型。
- 它以传递给该函数成员的实参值(如果存在)和实例值的副本进行初始化。
可枚举对象通常是编译器生成的可枚举类的实例,它封装了迭代器块中的代码,并实现了可枚举接口,但也可能实现其他方法。如果可枚举类由编译器生成,则该类将直接或间接嵌套在包含该函数成员的类中,它将具有私有可访问性,并且它将具有供编译器使用的保留名称(第 2.4.2 节)。
可枚举对象可实现除上面指定的那些接口以外的其他接口。具体而言,可枚举对象还可实现 IEnumerator 和 IEnumerator<T>,从而使其既可作为可枚举对象,也可作为枚举器对象。在该类型的实现中,首次调用可枚举对象的 GetEnumerator 方法时,将返回可枚举对象本身。对可枚举对象的 GetEnumerator 的后续调用(如果存在),将返回可枚举对象的副本。因此,每个返回的枚举器都有自己的状态,一个枚举器中的更改不会影响其他枚举器。
1.14.5.1 GetEnumerator 方法
可枚举对象实现了 IEnumerable 和 IEnumerable<T> 接口的 GetEnumerator 方法。这两种 GetEnumerator 方法的实现是相同的,都是获取并返回一个可用的枚举器对象。枚举器对象是以初始化该可枚举对象时保存的实例值和实参值进行初始化的,此外,枚举器对象函数如第 10.14.4 节所述。
1.14.6 实现示例
本节从标准 C# 构造的角度描述迭代器可能的实现。此处所描述的实现基于 Microsoft C# 编译器所使用的相同原理,但决非是强制性的实现方式,也不是唯一可能的实现方式。
下面的 Stack<T> 类使用一个迭代器实现其 GetEnumerator 方法。该迭代器以自顶向下的顺序枚举堆栈的元素。
using System;
using System.Collections;
using System.Collections.Generic;
class
Stack<T>: IEnumerable<T>
{
T[] items;
int count;
public void Push(T item) {
if (items == null) {
items = new T[4];
}
else if (items.Length == count) {
T[] newItems = new T[count * 2];
Array.Copy(items, 0, newItems, 0,
count);
items = newItems;
}
items[count++] = item;
}
public T Pop() {
T result = items[--count];
items[count] = default(T);
return result;
}
public IEnumerator<T> GetEnumerator() {
for (int i = count - 1; i >= 0;
--i) yield return items[i];
}
}
GetEnumerator 方法可转换为编译器生成的枚举器类的实例化,该类封装了迭代器块中的代码,如下所示。
class
Stack<T>: IEnumerable<T>
{
...
public IEnumerator<T> GetEnumerator() {
return new __Enumerator1(this);
}
class __Enumerator1: IEnumerator<T>,
IEnumerator
{
int __state;
T __current;
Stack<T> __this;
int i;
public __Enumerator1(Stack<T> __this)
{
this.__this = __this;
}
public T Current {
get { return __current; }
}
object IEnumerator.Current {
get { return __current; }
}
public bool MoveNext() {
switch (__state) {
case 1: goto __state1;
case 2: goto __state2;
}
i = __this.count - 1;
__loop:
if (i < 0) goto __state2;
__current = __this.items[i];
__state = 1;
return true;
__state1:
--i;
goto __loop;
__state2:
__state = 2;
return false;
}
public void Dispose() {
__state = 2;
}
void IEnumerator.Reset() {
throw new NotSupportedException();
}
}
}
在前面的转换中,迭代器块中的代码转换为状态机,并置于枚举器类的 MoveNext 方法中。此外,局部变量 i 转换为枚举器对象中的字段,这样它就可以在 MoveNext 的多次调用之间继续存在。
下面的示例打印整数 1 到 10 的简单乘法表。该示例中的 FromTo 方法使用迭代器实现,并且返回一个可枚举对象。
using System;
using System.Collections.Generic;
class Test
{
static IEnumerable<int> FromTo(int
from, int to) {
while (from <= to) yield return
from++;
}
static void Main() {
IEnumerable<int> e = FromTo(1,
10);
foreach (int x in e) {
foreach (int y in e) {
Console.Write("{0,3}
", x * y);
}
Console.WriteLine();
}
}
}
FromTo 方法可转换为编译器生成的可枚举类的实例化,该类封装了迭代器块中的代码,如下所示。
using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
class Test
{
...
static IEnumerable<int> FromTo(int from,
int to) {
return new __Enumerable1(from, to);
}
class __Enumerable1:
IEnumerable<int>, IEnumerable,
IEnumerator<int>, IEnumerator
{
int __state;
int __current;
int __from;
int from;
int to;
int i;
public __Enumerable1(int __from, int to) {
this.__from = __from;
this.to = to;
}
public IEnumerator<int>
GetEnumerator() {
__Enumerable1 result = this;
if
(Interlocked.CompareExchange(ref __state, 1, 0) != 0) {
result = new
__Enumerable1(__from, to);
result.__state = 1;
}
result.from
= result.__from;
return result;
}
IEnumerator IEnumerable.GetEnumerator() {
return
(IEnumerator)GetEnumerator();
}
public int Current {
get { return __current; }
}
object IEnumerator.Current {
get { return __current; }
}
public bool MoveNext() {
switch (__state) {
case 1:
if (from > to) goto case 2;
__current = from++;
__state = 1;
return true;
case 2:
__state = 2;
return false;
default:
throw new
InvalidOperationException();
}
}
public void Dispose() {
__state = 2;
}
void IEnumerator.Reset() {
throw new NotSupportedException();
}
}
}
可枚举类同时实现了可枚举接口和枚举器接口,使其既可作为可枚举对象,也可作为枚举器对象。首次调用 GetEnumerator 方法时将返回该可枚举对象本身。对可枚举对象的 GetEnumerator 的后续调用(如果存在),将返回可枚举对象的副本。因此,每个返回的枚举器都有自己的状态,一个枚举器中的更改不会影响其他枚举器。Interlocked.CompareExchange 方法用于确保线程安全操作。
from 和 to 形参转换为可枚举类中的字段。因为 from 是在迭代器块中修改的,所以引入附加 __from 字段以保存提供给每个枚举器中的 from 的初始值。
如果在 __state 为 0 时调用 MoveNext 方法,则该方法将引发 InvalidOperationException。这可防止在未事先调用 GetEnumerator 的情况下将可枚举对象用作枚举器对象。
下面的示例演示一个简单的树类。Tree<T> 类使用一个迭代器实现其 GetEnumerator 方法。迭代器按照中缀顺序枚举树的元素。
using System;
using System.Collections.Generic;
class
Tree<T>: IEnumerable<T>
{
T value;
Tree<T> left;
Tree<T> right;
public Tree(T value, Tree<T> left,
Tree<T> right) {
this.value = value;
this.left = left;
this.right = right;
}
public IEnumerator<T> GetEnumerator() {
if (left != null) foreach (T x in
left) yield x;
yield value;
if (right != null) foreach (T x in
right) yield x;
}
}
class Program
{
static Tree<T>
MakeTree<T>(T[] items, int left, int right) {
if (left > right) return null;
int i = (left + right) / 2;
return new Tree<T>(items[i],
MakeTree(items, left, i - 1),
MakeTree(items, i + 1, right));
}
static Tree<T> MakeTree<T>(params
T[] items) {
return MakeTree(items, 0,
items.Length - 1);
}
// The output of the program is:
// 1 2 3 4 5 6 7 8 9
// Mon Tue Wed Thu Fri Sat Sun
static void Main() {
Tree<int> ints = MakeTree(1, 2,
3, 4, 5, 6, 7, 8, 9);
foreach (int i in ints) Console.Write("{0}
", i);
Console.WriteLine();
Tree<string> strings = MakeTree(
"Mon", "Tue",
"Wed", "Thu", "Fri", "Sat",
"Sun");
foreach (string s in strings)
Console.Write("{0} ", s);
Console.WriteLine();
}
}
GetEnumerator 方法可转换为编译器生成的枚举器类的实例化,该类封装了迭代器块中的代码,如下所示。
class
Tree<T>: IEnumerable<T>
{
...
public IEnumerator<T> GetEnumerator() {
return new __Enumerator1(this);
}
class __Enumerator1 : IEnumerator<T>,
IEnumerator
{
Node<T> __this;
IEnumerator<T> __left, __right;
int __state;
T __current;
public __Enumerator1(Node<T> __this)
{
this.__this = __this;
}
public T Current {
get { return __current; }
}
object IEnumerator.Current {
get { return __current; }
}
public bool MoveNext() {
try {
switch (__state) {
case 0:
__state = -1;
if (__this.left == null)
goto __yield_value;
__left =
__this.left.GetEnumerator();
goto case 1;
case 1:
__state = -2;
if (!__left.MoveNext())
goto __left_dispose;
__current = __left.Current;
__state = 1;
return true;
__left_dispose:
__state = -1;
__left.Dispose();
__yield_value:
__current = __this.value;
__state = 2;
return true;
case 2:
__state = -1;
if (__this.right == null)
goto __end;
__right =
__this.right.GetEnumerator();
goto case 3;
case 3:
__state = -3;
if (!__right.MoveNext())
goto __right_dispose;
__current =
__right.Current;
__state = 3;
return true;
__right_dispose:
__state = -1;
__right.Dispose();
__end:
__state = 4;
break;
}
}
finally {
if (__state < 0) Dispose();
}
return false;
}
public void Dispose() {
try {
switch (__state) {
case 1:
case -2:
__left.Dispose();
break;
case 3:
case -3:
__right.Dispose();
break;
}
}
finally {
__state = 4;
}
}
void IEnumerator.Reset() {
throw new NotSupportedException();
}
}
}
foreach 语句中使用的编译器生成的临时变量被提升为枚举器对象的 __left 和 __right 字段。枚举器对象的 __state 字段得到了妥善的更新,以便在引发异常时正确地调用正确的 Dispose() 方法。注意,不可能使用简单的 foreach 语句写入转换后的代码。
1.15 异步函数
带有 async 修饰符的方法(第 10.6 节)或匿名函数(第 7.15 节)称为异步函数。通常,术语异步用于描述具有 async 修饰符的任何类型的函数。
在异步函数的形参列表中指定任何 ref 或 out 参数会导致编译时错误。
异步方法的 return-type 必须为 void 或任务类型。任务类型为 System.Threading.Tasks.Task,且类型从 System.Threading.Tasks.Task<T> 构建。为简洁起见,本章中将这些类型分别表示为 Task 和 Task<T>。返回任务类型的异步方法称为返回任务的方法。
任务类型的确切定义是由实现定义的,但从语言的角度来看,任务类型的状态分为未完成、成功或出错。出错任务将记录相关异常。succeeded Task<T>记录类型 T 的结果。任务类型是可等待的,因此任务可以作为 await 表达式(第 7.7.7 节)的操作数。
异步函数调用可以通过其主体中的 await 表达式(第 7.7.7 节)挂起计算。计算可以在稍后通过恢复委托挂起 await 表达式时恢复。恢复委托的类型为 System.Action,调用它时,异步函数调用的计算将从 await 表达式的中止位置恢复。如果异步函数调用从未挂起,则该函数调用的当前调用方为原始调用方,否则为恢复委托的最近一次调用方。
1.15.1 返回任务的异步函数计算
调用返回任务的异步函数将导致生成返回的任务类型的实例。这称为异步函数的返回任务。任务最初处于未完成状态。
然后,异步函数体将一直计算,直到它挂起(由于到达 await 表达式)或终止,此时控制将返回到调用方,并返回任务。
当异步函数体终止时,返回的任务将脱离未完成状态:
- 如果由于到达 return 语句或函数体末尾而导致函数体终止,则任何结果值均将记录在返回任务中,并将该任务置于成功状态。
- 如果由于未捕获的异常(第 8.9.5 节)而导致函数体终止,该异常将记录在返回任务中,并将该任务置于出错状态。
1.15.2 返回 void 的异步函数计算
如果异步函数的返回类型是 void,则计算将在以下方面不同于上述函数:由于没有任务返回,该函数改为将完成状态和异常传递给当前线程的同步上下文。同步上下文的确切定义取决于实现,而且只是当前运行的线程所在“位置”的表示形式。返回 void 的异步函数的计算在开始、成功完成或导致引发未捕获的异常时,均会通知同步上下文。
这将使得上下文可以跟踪其下有多少返回 void 的异步函数正在运行,并决定如何传播由这些函数产生的异常。