-- 作者:卷积内核
-- 发布时间:2/4/2010 8:48:00 AM
--
匿名方法的局部变量用法 到现在为止,我们对匿名方法如何工作以及内部如何实现有了一点基本的理解。从根本上说,C#创建了private方法来包装匿名方法。同时这些方法的签名与它们被分配到的委托相匹配。现在,让我们看看下面的代码: public class Program { public delegate void MyDelegate(); public static void Main(string[] args) { int iTemp = 100; MyDelegate dlg = delegate { Console.WriteLine(iTemp); }; dlg(); } } 对于我们到现在为止对匿名方法已了解的内容来说,这段代码不应该编译。因为我们没有使用如何实例数据成员,C#编译器应该在''Program''类中创建一个private静态方法来包装这个匿名方法。但是新的方法如何访问局部变量呢?这让我们相信该代码将不能被编译。但是令人惊讶的是,C#编译器成功编译了这个代码而没有任何错误或报警。而且,当你执行这个示例时,在控制台屏幕上输出打印出iTemp变量的正确的值。现在让我们进入匿名方法的高级话题。一个匿名方法有封装在其方法体中使用了的环境变量的值的能力。这个封装应用于匿名方法被定义的方法中的所有局部变量。当C#编译器在一个匿名方法的方法体中识别出用到一个局部变量,它就会做如下事情: 1. 创建一个新的private类作为匿名方法被定义的类的一个内部类。 2. 在新类(译注:即内部类)中创建一个公共数据成员,使用与用在匿名方法体中的局部变量相同的类型和名称。 3. 在包装匿名方法的新类中创建一个public实例方法。 4. 用新类中的声明替代局部变量的声明。创建该新类的一个实例代替局部变量的声明。 5. 用新类实例的数据成员替代在匿名方法体内部和外部使用的局部变量。 6. 用在新类中定义的实例方法的地址取代匿名方法的定义。 因此在编译时,上面的代码将被C#编译器翻译为如下代码: public class Program { private class InnerClass { private void InstanceMethod() { Console.WriteLine(iTemp); } public int iTemp; } public delegate void MyDelegate(); public static void Main(string[] args) { InnerClass localObject = new InnerClass(); localObject.iTemp = 100; MyDelegate dlg = new MyDelegate(localObject.InstanceMethod); dlg(); } } 正如上面的伪代码所示,C#编译器为''Program''类生成了一个private内部类。在匿名方法中使用的局部变量作为新的已创建的内部类的一个实例数据成员而捕获。并且匿名方法本身被包装在内部类的实例方法中。最后,该实例方法在Main方法中作为一个委托处理器而使用。这样,当委托被调用时,对于在被封装入匿名方法中的局部变量将会有一个正确的值。下面图中选定的部分显示了由C#编译器默默添加到''Program'' 类的新的private内部类。  被用在匿名方法中的局部变量有着超出用到它们的外部常规方法的生命周期。这个技术,在其它语言中,就是大家都知道的closures。除去匿名方法提供的简单语法,closures是匿名方法提供给开发者的一个功能强大的技术。该技术允许委托处理器代码(匿名方法)访问在常规方法内部被定义的局部变量。这就允许out-of-band数据,除了委托参数之外还有数据将被传递到委托,以供在其方法执行时使用。没有这个技术,每个委托和其相应的处理器方法就不得不声明表示局部上下文数据的参数,随着时间的过去这(译注:指不断声明表示局部上下文数据的参数)将变得难于管理。 匿名方法的作用域和局部变量用法 我们讨论了在方法的主作用域(the main scope)中的匿名方法的实现。当一个匿名方法在一个嵌套作用域中被定义时,并且匿名方法中用到独立作用域级的局部变量,C#为每个作用域创建一个private内部类。比如,假设scope 1有局部变量iTemp,而scope 2,是scope 1的嵌套作用域,有一个局部变量jTemp。让在使用来自scope 1 和 scope 2局部变量iTemp 和 jTemp的 scope 2中,我们定义一个匿名方法。下面的代码显示了上面描述的示例: public class Program { public delegate void MyDelegate(); public static void Main(string[] args) { MyDelegate dlg = null; int iTemp = 100; if (iTemp > 50) { int jTemp = 200; dlg = delegate { Console.WriteLine("iTemp: {0}, jTemp: {1}",iTemp,jTemp); }; } dlg(); } } 当上面的代码被编译时,C#编译器在''Program''类中创建两个内部类。一个内部类包装局部变量iTemp作为一个public数据成员。第二个内部类包装在嵌套作用域中的局部变量,jTemp,作为一个public数据成员,同时在相同的嵌套作用域中包装匿名方法作为public实例方法。C#编译器为上面的代码生成下面的伪代码: public class Program { //包装来自外部作用域的局部变量''iTemp''的类 private class InnerClassScope1 { public int iTemp; } //包装来自内部作用域和匿名方法的局部变量的类 private class InnerClassScope2 { public void InstanceMethod() { Console.WriteLine("iTemp: {0}, jTemp: {1}", localObjectScope1.iTemp, jTemp); } public InnerClassScope1 localObjectScope1; public int jTemp; } public delegate void MyDelegate(); public static void Main(string[] args) { MyDelegate dlg = null; InnerClassScope1 localObject1 = new InnerClassScope1(); localObject1.iTemp = 100; if (localObject1.iTemp > 50) { InnerClassScope2 localObject2 = new InnerClassScope2(); localObject2.localObjectScope1 = localObject1; localObject2.jTemp = 200; dlg = new MyDelegate(localObject2.InstanceMethod); } dlg(); } } 正如上面的代码所示,包装匿名方法的内部类将拥有所有代表外部作用域局部变量的对象,这些变量被用在匿名方法中,像public数据成员。下图显示了C#默默创建的内部类的ILDASM视图:  在循环控制结构内使用匿名方法的局部变量的用法 当处理循环控制结构时将局部变量封装入类的数据成员有着有趣但危险的一面,让我们看看下面代码: public class Program { public delegate void MyDelegate(); public static void Main(string[] args) { MyDelegate d = null; for (int i = 1; i <= 5; i++) { MyDelegate tempD = delegate { Console.WriteLine(i); }; d += tempD; } d(); } } 上面的代码运行时将会有什么输出呢?我们的意图是捕获在我们的匿名方法中的循环计数变量''i''并显示之。我们预期的输出应该如下所示: 1 2 3 4 5 但是如果你运行上面的代码,输出将是如下所示: 6 6 6 6 6 如果我们仔细回忆我们关于匿名方法的内部工作机制的知识,我提到:在匿名方法中被捕获的任何局部变量将会被该作用域的一个新的已创建内部类的实例数据成员替代。对于循环控制变量,作用域是包含了for循环的作用域,这就是上面的简单代码所示的main方法体。因此当该代码编译时,C#编译器生成创建了内部类的实例的代码,包装了匿名方法和循环计数变量,在for循环的外部。并且该内部类的实例的数据成员,代表了循环计数变量,将被用来替代用于for循环而且也在匿名方法中使用的原始循环计数变量。因此来自内部类的相同实例的数据成员被用于for循环并且也用在包装匿名方法的实例方法中。作为循环完成时的结果,实例数据成员会增加六次。这里有一个需要注意的重要地方:尽管这个循环在五次迭代后结束,在它跳出循环控制结构时循环计数变量被增加了六次。既然该循环控制变量是一个实例数据成员,第六次增加触发了已由循环计数变量提供的循环结束条件。既然相同实例的一个方法被用做匿名方法的委托处理器,在委托结束时被调用,所有委托的实例将被指向相同实例,同时将为数据成员显示相同值,就是6。这就是我在本节开始已提到过的有危险影响的一面。 为了克服这个问题并获得预期的结果,匿名方法应该在for循环的作用域中捕获一个局部变量,它将有与循环计数变量的相同的值。这可以通过如下修改示例代码获得: public class Program { public delegate void MyDelegate(); public static void Main(string[] args) { MyDelegate d = null; for (int i = 1; i <= 5; i++) { int k = i; MyDelegate tempD = delegate { Console.WriteLine(k); }; d += tempD; } d(); } } 在你运行上面的代码示例时,将会获得预期的输出,也就是: 1 2 3 4 5 原因就是,C#编译器将为for循环的每次迭代而包装局部变量''k''的内部类创建 实例。同时包装了每个循环迭代的实例上的匿名方法的这个方法被用做一个委托处理器。 总结 匿名方法是C#2.0语言增加的一个非常有用和强大的功能。除了介绍的一些对委托声明和用法上的语法改进,Microsoft已在使匿名方法代码自然融入所包含的方法体方面获得很大进展,包括访问在包含(匿名方法)的方法定义的作用域中的局部变量。最后,我希望本文提供给C#开发人员正确而聪明地利用匿名方法的必备知识。
|