第三章 匿名方法
原著:Microsoft Corporation
原文:http://msdn.microsoft.com/vcsharp/team/language/default.aspx (SpecificationVer2.doc)
翻译:lover_P
出处:
--------------------------------------------------------------------------------
[内容]
3.1 匿名方法表达式
3.2 匿名方法签名
3.3 匿名方法转换
3.3.1 委托建立表达式
3.4 匿名方法块
3.5 外部变量
3.5.1 捕获外部变量
3.5.2 局部变量的实例化
3.6 匿名方法求值
3.7 委托实例相等性
3.8 明确赋值
3.9 方法组转换
3.10 实现实例
3.1 匿名方法表达式
匿名方法表达(anonymous-method-expression)式定义了匿名方法(anonymous method),并求得一个引用了该方法的特殊的值。
primary-no-array-creation-expression:
…
anonymous-method-expression
anonymous-method-expression:
delegate anonymous-method-signatureopt block
anonymous-method-signature:
( anonymous-method-parameter-listopt )
anonymous-method-parameter-list:
anonymous-method-parameter
anonymous-method-parameter-list , anonymous-method-parameter
anonymous-method-parameter:
parameter-modifieropt type identifier
初等非数组表达式:
...
匿名方法表达式
匿名方法表达式:
delegate 匿名方法签名可选 块
匿名方法签名:
( 匿名方法参数列表可选 )
匿名方法参数列表:
匿名方法参数
匿名方法参数 , 匿名方法参数
匿名方法参数:
参数修饰符可选 类型 标识符
匿名方法表达(anonymous-method-expression)是一个遵从特殊转换规则(见3.3)的值。这个值没有类型,但可以隐式地转换为一个兼容的委托类型。
匿名方法表达式(anonymous-method-expression)为参数、局部变量和常量以及标签定义了一个新的声明空间。
3.2 匿名方法签名
可选的匿名方法签名(anonymous-method-signature)为匿名方法的形式参数定义了名字和类型。这些参数的作用于是匿名方法的块(block)。如果一个局部变量的作用域包含了匿名方法表达式(anonymous-method-expression),且该匿名方法的参数和该局部变量相同,则会产生编译错误。
如果一个匿名方法表达式(anonymous-method-expression)具有匿名方法签名(anonymous-method-signature),则与之兼容的委托类型被强制具有相同的参数类型和修饰符,且具有相同顺序(见3.3)。如果一个匿名方法表达式(anonymous-method-expression)没有匿名方法签名(anonymous-method-signature),则与之相兼容的委托类型被强制要求没有out参数。
注意匿名方法签名(anonymous-method-signature)不能包含特性或参数数组(译注:用于实现变长参数列表)。然而,一个匿名方法签名(anonymous-method-signature)可以和一个包含参数数组的委托类型相兼容。
3.3 匿名方法转换
匿名方法表达式(anonymous-method-expression)是一个没有类型的特殊值。一个匿名方法表达式(anonymous-method-expression)可以用于委托建立表达式(delegate-creation-expression)(见3.3.1)。对于匿名方法表达式(anonymous-method-expression)的其他有效的应用取决于定义于其上的隐式转换。
匿名方法表达式(anonymous-method-expressio)与任何兼容的委托类型之间均存在隐式转换。如果D是一个委托类型,而A是一个匿名方法表达式(anonymous-method-expression),当且仅当以下两个条件成立的时候D和A是兼容的。
首先,D的参数类型必须与A兼容:
如果A不含匿名方法签名(anonymous-method-signature),则D可以具有任意类型的零或多个参数,但这些参数不能带有out修饰符。
如果具有匿名方法签名(anonymous-method-signature),则D必须具有和A形同数量的参数,A中的每个参数必须和D中相应的参数具有相同的类型,并且A中每个参数上的ref或out修饰符的出现与否必须与D中的相应参数相同。如果D中的最后一个参数是参数数组,则没有相互兼容的A和D。
其次,D的返回值类型必须与A兼容。由于这一规则,A中不能包含其他匿名方法的块(block)。
如果D的返回值类型被声明为void,则A中包含的所有return语句不能指定表达式。
如果D的返回值类型被声明为R,则A中包含的所有return语句不许指定一个能够隐式转换为R的表达式。A中的块(block)的终点必须可达。
除了和相兼容的委托类型之间的隐式转换,匿名方法表达式(anonymous-method-expression)与任何类型之间不存在任何转换,包括object类型。
下面的例子详细地解释了这些规则:
delegate void D(int x);
D d1 = delegate { }; // 正确
D d2 = delegate() { }; // 错误,签名不匹配
D d3 = delegate(long x) { }; // 错误,签名不匹配
D d4 = delegate(int x) { }; // 正确
D d5 = delegate(int x) { return; }; // 正确
D d6 = delegate(int x) { return x; }; // 错误,返回值不匹配
delegate void E(out int x);
E e1 = delegate { }; // 错误,E带有一个输出参数
E e2 = delegate(out int x) { x = 1; }; // 正确
E e3 = delegate(ref int x) { x = 1; }; // 错误,签名不匹配
delegate int P(params int[] a);
P p1 = delegate { }; // 错误,块的结尾不可达
P p2 = delegate { return; }; // 错误,返回值类型不匹配
P p3 = delegate { return 1; }; // 正确
P p4 = delegate { return "Hello"; }; // 错误,返回值类型不匹配
P p5 = delegate(int[] a) { // 正确
return a[0];
};
P p6 = delegate(params int[] a) { // 错误,(译注:多余的)params修饰符
return a[0];
};
P p7 = delegate(int[] a) { // 错误,返回值类型不匹配
if(a.Length > 0) return a[0];
return "Hello";
};
delegate object Q(params int[] a);
Q q1 = delegate(int[] a) { // 正确
if(a.Length > 0) return a[0];
return "Hello";
};
3.3.1 委托建立表达式
委托建立表达式(delegate-creation-expression)可以用于匿名方法和委托类型之间的转换语法的替代品。如果一个委托建立表达式(delegate-creation-expression)的参数表达式(expression)是一个匿名方法表达式(anonymous-method-expression),则匿名方法依照上面定义的隐式转换规则转换为给定的委托类型。例如,如果D是一个委托类型,则表达式
new D(delegate { Console.WriteLine("hello"); })
等价于表达式
(D) delegate { Console.WriteLine("hello"); }
3.4 匿名方法块
匿名方法表达式(anonymous-method-expression)的块(block)遵从下列规则:
如果匿名方法包含一个签名,则签名中指定的参数在块(block)中是可用的。如果匿名方法不包含签名,则它可以转换为一个带有参数的委托类型(见3.3),但这些参数在块(block)中无法访问。
除非在最贴切的匿名方法的签名(如果有的话)中指定了ref或out参数,否则在块中访问ref或out参数会发生编译期间错误。
当this的类型是一个结构类型时,当在块(block)中访问this是一个编译错误,不论这种能够访问是显式的(如this.x)还是隐式的(如x,而x是该结构的一个实例方法)。这一规则简单地禁止了这种访问,从而不会对结构的成员的查找结果产生影响。
块(block)可以访问匿名方法外部的变量(见3.5)。对外部变量的访问将会引用到变量的实例,该变量在匿名方法表达式(anonymous-method-expression)求值的过程中应当是活动的(见3.6)。
如果块(block)中包含的goto语句、break语句或continue语句的目标在块(block)的外面或在块(block)中包含的一个匿名方法中,则会产生编译错误。
块(block)中的return语句将控制从最近的一个匿名方法的调用中返回,而不是从函数成员中返回。return语句中指定的表达式必须和匿名方法表达式(anonymous-method-expression)转换(见3.3)得到的委托类型相匹配。
除了计算和调用匿名方法表达式(anonymous-method-expression)外,对于块(block)的执行方式没有任何明确的限制。特别地,编译器会选择通过合成个或多个命名了的方法或类型来实现一个匿名方法。这些合成的名字必须保留在编译器所使用的空间中:这些名字必须包含两个连续的下划线。
3.5 外部变量
若一个匿名方法表达式(anonymous-method-expression)包含在任何局部变量、值参数或参数数组的作用域中,则称它们(译注:指那些局部变量、值参数或参数数组)为该匿名方法表达式(anonymous-method-expression)的外部变量(outer variable)。在一个类的实例函数成员中,this被认为是一个值参数,并且是该函数成员中所包含的任何匿名方法表达式(anonymous-method-expression)的外部变量。
3.5.1 捕获外部变量
当在一个匿名方法中引用一个外部变量时,称该外部变量被匿名方法所捕获。通常,一个局部变量的生存期是块或与之关联的语句的执行的结束点。然而,一个被捕获的外部变量的生存期将持续到引用了匿名方法的委托符合垃圾收集的条件时。
在下面的例子中:
using System;
delegate int D();
class Test {
static D F() {
int x = 0;
D result = delegate { return ++x; }
return result;
}
static void Main() {
D d = F();
Console.WriteLine(d());
Console.WriteLine(d());
Console.WriteLine(d());
}
}
局部变量x被匿名方法所捕获,x的生存期至少被延续到从F所返回的委托符合垃圾收集条件时(这并不一定发生在程序的最末尾)。由于每个匿名方法的调用均操作了x的同一个实例,这个例子的输出将会是:
1
2
3
当一个局部变量或一个值参数被一个匿名方法所捕获,该局部变量获知参数将不再被认为是一个固定变量,而是被认为是一个可移动的变量。因此,任何unsafe代码如果记录了该外部变量的地址,则必须首先使用fixed语句来固定这个变量。
3.5.2 局部变量的实例化
当程序执行到一个变量的作用域中时,则该局部变量被实例化。例如,当下面的方法被调用时,局部变量x被实例化和初始化三次——每当循环迭代一次时。
static void F() {
for(int i = 0; i < 3; i++) {
int x = i * 2 + 1;
...
}
}
然而,将x的声明移到循环外面则只会引起x的一次实例化:
static void F() {
int x;
for(int i = 0; i < 3; i++) {
x = i * 2 + 1;
...
}
}
通常,没有办法观察到一个局部变量被实例化过多少次——因为实例化的生存期是脱节的,而上每次实例化都简单地使用相同的存贮空间是可能的。然而,当一个匿名方法捕获了一个局部变量,实例化的效果就明显了。下面的例子
using System;
delegate void D();
class Test {
static D[] F() {
D[] result = new D[3];
for(int i = 0; i < 3; i++) {
int x = i * 2 + 1;
result[i] = delegate { Console.WriteLine(x); };
}
return result;
}
static void Main() {
foreach (D d in F()) d();
}
}
的输出为:
1
3
5
然而,当x的声明被移到循环外面时:
static D[] F() {
D[] result = new D[3];
int x;
for(int i = 0; i < 3; i++) {
x = i * 2 + 1;
result[i] = delegate { Console.WriteLine(x); };
}
return result;
}
结果为:
5
5
5
注意,根据判等操作(见3.7),由上面这个版本的F方法所建立的三个委托是相等的。另外,编译器可以(但不是必须)将这三个实例优化为一个单独的委托实例(见3.6)。
匿名方法委托可以共享一些捕获的变量,而具有另一些捕获变量的单独的实例。例如,如果F变为
static D[] F() {
D[] result = new D[3];
int x = 0;
for(int i = 0; i < 3; i++) {
int y = 0;
result[i] = delegate { Console.WriteLine("{0} {1}", ++x, ++y); };
}
return result;
}
这三个委托捕获了x的相同的(一个)实例,而捕获y的单独的实例。输出为:
1 1
2 1
3 1
单独的匿名方法可以捕获一个外部变量的相同的实例。下面的例子中:
using System;
delegate void Setter(int value);
delegate int Getter();
class Test {
static void Main() {
int x = 0;
Setter s = delegate(int value) { x = value; };
Getter g = delegate { return x; };
s(5);
Console.WriteLine(g());
s(10);
Console.WriteLine(g());
}
}
两个匿名方法捕获了局部变量x的相同的实例,它们可以通过改变量进行“通信”。这个例子的输出为:
5
10
3.6 匿名方法求值
运行时对匿名方法表达式的求值将得到一个委托实例,该委托实例引用了这个匿名方法和一组活动的外部变量的集合(可能为空)。当一个由匿名方法表达式求得的委托被调用时,匿名方法体将被执行。方法体中的代码可以使用由委托所引用的一组捕获了的外部变量。
有匿名方法表达式得到的委托的调用链表包含一个唯一的入口点。委托的确切的目标对象和目标方法是未定义的。尤其是委托的目标对象是否为null、函数成员内部的this值或其他对象也是未定义的。
对带有相同的一组(可能为空)捕获的外部变量的语义上一致的匿名方法表达式的求值可以(但不是必须)返回相同的委托实例。这里用的术语“语义上一致的”表示任何情况下对匿名方法的执行对同样的参数应该产生同样的效果。这一规则允许对代码作如下优化:
delegate double Function(double x);
class Test {
static double[] Apply(double[] a, Function f) {
double[] result = new double[a.Length];
for(int i = 0; i < a.Length; i++) result[i] = f(a[i]);
return result;
}
static void F(double[] a, double[] b) {
a = Apply(a, delegate(double x) { return Math.Sin(x); });
b = Apply(b, delegate(double y) { return Math.Sin(y); });
...
}
}
由于两个匿名方法委托具有相同的一组捕获的外部变量(为空),而且匿名方法在语义上是一致的,编译器可以令这两个委托引用一个相同的目标方法。甚至对于这两个匿名方法表达式,编译器可以返回完全一样的委托实例。
3.7 委托实例相等性
下面的规则决定着匿名方法委托实例的判等操作符和Object.Equals方法的结果:
从语义上相同的带有相同一组(也可能是都没有没有)被捕获变量的匿名方法表达式(anonymous-method-expressions)所产生的委托实例可以(但不是必须)相等。
从语义上相同的单被捕获变量不同的的匿名方法表达式(anonymous-method-expressions)所产生的委托实例必须不相等。
3.8 明确赋值
对匿名方法参数的明确赋值规则和命名方法(named method,区别于匿名方法)参数的明确复制规定一样。也就是说,引用参数和值参数必须同国明确的赋值进行初始化,而输出参数可以不比进行明确赋值。
另外,如果要在匿名方法正常返回之前使用输出参数,则输出参数必须被明确赋值。
当控制转移到匿名方法表达式的块中时,对于一个外部变量v的明确赋值规定与匿名方法表达式之前对v的明确赋值规定一样。
对于一个匿名方法后面的变量v的明确赋值规定和匿名方法表达式之前的明确赋值规定相同。
下面的例子:
delegate bool Filter(int i);
void F() {
int max;
// 错误,max没有被明确赋值
Filter f = delegate(int n) { return n < max; }
max = 5;
DoWork(f);
}
会产生一个编译错误,因为在匿名方法声明之前max没有被明确赋值。下面的例子
delegate void D();
void F() {
int n;
D d = delegate { n = 1; };
d();
// 错误,n没有被明确赋值
Console.WriteLine(n);
}
同样会产生变异错误,因为匿名方法中对n的赋值不会影响匿名方法外部对n的明确赋值。
3.9 方法组转换
和3.3节中描述的匿名方法隐式转换类似,从一个方法组到一个兼容的委托类型之间也存在一个隐式的转换。
对于一个给定的方法组E和一个委托类型D,如果允许形为new D(E)的委托建立表达式(见2.9.6),则存在E到D的隐式转换,
下面的例子:
using System;
using System.Windows.Forms;
class AlertDialog {
Label message = new Label();
Button okButton = new Button();
Button cancelButton = new Button();`
public AlertDialog() {
okButton.Click += new EventHandler(OkClick);
cancelButton.Click += new EventHandler(CancelClick);
...
}
void OkClick(object sender, EventArgs e) {
...
}
void CancelClick(object sender, EventArgs e) {
...
}
}
构造器使用两个new运算符建立了两个委托实例。隐式方法组转换允许将其写作更短的形式:
public AlertDialog() {
okButton.Click += OkClick;
cancelButton.Click += CancelClick;
...
}
与其它隐式和显式转换相同,转换运算符可以显式地用于一个特定的转换中。因此,下面的例子:
object obj = new EventHandler(myDialog.OkClick);
可以写作:
object obj = (EventHandler)myDialog.OkClick;
方法组和匿名方法表达式会影响到重载抉择,但不会参与类型推断。更多细节参见2.6.4节
3.10 实现实例
这一节将讨论一些标准C#构造中匿名方法的可能的实现。这里描述的实现和Visual C#编译器基于相同的原理,但这并不是必须的实现,而只是一种可能。
本节的最后将给出包含了不同特征的匿名方法的代码实例。对于每个例子,转换得到的相应代码仅使用了标准C#提供的构造。这些例子中,假设标识符D为下面这样的委托类型:
public delegate void D();
最简单的匿名方法是不捕获任何外部变量的匿名方法:
class Test {
static void F() {
D d = delegate { Console.WriteLine("test"); };
}
}
它会被翻译为一个委托实例,它引用了一个编译器产生的静态方法,这个方法中包含了匿名方法中的代码:
class Test {
static void F() {
D d = new D(__Method1);
}
static void __Method1() {
Console.WriteLine("test");
}
}
In the following example, the anonymous method references instance members of this:
下面的例子中,你名方法引用了this的实例成员:
class Test {
int x;
void F() {
D d = delegate { Console.WriteLine(x); };
}
}
This can be translated to a compiler generated instance method containing the code of the anonymous method:
它会被翻译为一个编译器生成的实例方法,该方法包含了匿名方法中的代码:
class Test {
int x;
void F() {
D d = new D(__Method1);
}
void __Method1() {
Console.WriteLine(x);
}
}
In this example, the anonymous method captures a local variable:
在这个例子中,匿名方法捕获了一个局部变量:
class Test {
void F() {
int y = 123;
D d = delegate { Console.WriteLine(y); };
}
}
The lifetime of the local variable must now be extended to at least the lifetime of the anonymous method delegate. This can be achieved by “lifting” the local variable into a field of a compiler generated class. Instantiation of the local variable (§21.5.2) then corresponds to creating an instance of the compiler generated class, and accessing the local variable corresponds to accessing a field in the instance of the compiler generated class. Furthermore, the anonymous method becomes an instance method of the compiler generated class:
局部变量的生存期现在必须扩展为至少持续到匿名方法委托的生存期结束。这可以通过将局部变量“提升”到一个编译器生成的类的域中来完成。局部变量的实例化(见3.5.2)相当于建立编译器生成的类的一个实例,而访问局部变量相当于访问编译器建立的类的这个实例的域。另外,匿名方法变成编译器生成的类的一个实例方法:
class Test {
void F() {
__locals1 = new __Locals1();
__locals1.y = 123;
D d = new D(__locals1.__Method1);
}
class __Locals1 {
public int y;
public void __Method1() {
Console.WriteLine(y);
}
}
}
Finally, the following anonymous method captures this as well as two local variables with different lifetimes:
最后,下面的匿名方法捕获了this和两个具有不同生存期的局部变量:
class Test {
int x;
void F() {
int y = 123;
for (int i = 0; i < 10; i++) {
int z = i * 2;
D d = delegate { Console.WriteLine(x + y + z); };
}
}
}
Here, a compiler generated class is created for each statement block in which locals are captured such that the locals in the different blocks can have independent lifetimes. An instance of __Locals2, the compiler generated class for the inner statement block, contains the local variable z and a field that references an instance of __Locals1. An instance of __Locals1, the compiler generated class for the outer statement block, contains the local variable y and a field that references this of the enclosing function member. With these data structures it is possible to reach all captured outer variables through an instance of __Local2, and the code of the anonymous method can thus be implemented as an instance method of that class.
这里,编译器为每一个捕获了局部变量的语句块分别都生成了一个类。因此,那些在不同的块中所捕获的局部变量具有独立的生存期。编译器为内层语句块建立的类__Locals2的一个实例,包含了局部变量z和一个引用了__Locals1的实例的域。编译器为外层语句块建立的类__Locals1的一个实例,包含了局部变量y和一个引用了函数成员所在类的this的一个域。通过这些数据结构,可以通过__Locals2的一个实例来获得所有捕获的外部变量,而匿名方法中的代码因此被实现为该类的一个实例方法。
class Test {
void F() {
__locals1 = new __Locals1();
__locals1.__this = this;
__locals1.y = 123;
for (int i = 0; i < 10; i++) {
__locals2 = new __Locals2();
__locals2.__locals1 = __locals1;
__locals2.z = i * 2;
D d = new D(__locals2.__Method1);
}
}
class __Locals1 {
public Test __this;
public int y;
}
class __Locals2 {
public __Locals1 __locals1;
public int z;
public void __Method1() {
Console.WriteLine(__locals1.__this.x + __locals1.y + z);
}
}
}