原来是这样:C#中的闭包是怎么捕获变量的

我们知道,在匿名方法或者lambda中,可以访问或者修改该匿的定义范围内的变量。例如:

int num = 1;  
Func<int> incNum = () => ++num;

其中lambda表达式使用了在其外部定义的变量num。我们可以认为该段lambda语句块构成了一个闭包,而这个闭包捕获了外部变量num。

好了,不说那么多让人看着难受的定义套话了。我们进入正题,看看在C#中变量是如何被捕获的。来看一个例子:      

public Func<String> CreateFunction()  
{  
String str = "我的幸运数字是";  
int num = 17;  
Func<String> func = () => str + num;  
return func;
}

在这个例子中,定义了一个返回一个函数的方法CreateFunction。返回的函数构成了一个闭包,该闭包捕获了两个变量:String类型的str和int类型的num。

好了,我们现在可以这样使用这个函数了:  

Func<String> myFunc = CreateFunction();  
String result = myFunc();

 

 我们来分析一下这两行代码实际都干了什么。第一行很容易理解,我们把方法CreateFunction生成的匿名函数赋值给了委托myFunc。第二行更好理解,我们执行了myFunc,并将返回结果赋值给了变量result。我们再深入思考一下:在执行myFunc的时候,会访问到在CreateFunction中定义两个变量str与num。虽然这时CreateFunction的栈帧早就被销毁了,其内部定义的变量至今也“生死不明”了,但是因为我们知道这两个变量已经被闭包所捕获了,所以我们坚信这两个变量截至目前为止还是可以访问的!   

对于str对象,鉴于它是一个引用类型,所以只要有存在某个“东西”一直保存着对它的引用,它就不会被销毁。这样我们完全不用担心在我们需要它时,编译器或运行时会告诉我们它被弄丢了。然而对于num,情况就有些不同了。num是一个值类型。我们知道值类型是存活在栈上的,我们也知道它所存在的那个栈帧(也就是CreateFunction的帧)在CreateFunction执行完毕后就会被销毁,然后其上存在的任何值类型也会被一并的销毁,这其中当然包括我们所关注的变量num了。

 那么,我们为什么还能安全的访问num呢?C#中的变量捕获机制究竟有什么神奇之处,可以让值类型拥有违反常规的生存周期呢?装箱!你可能会立刻想到,把每个值类型都装到一个对象里,我们就可以让这个值类型拥有和那个包裹它的对象相同的寿命了。不过,这并不是C#实现者所选择的方式!C#并不会对每个需要捕获的值类型变量进行装箱操作,而是把所有捕获的变量统统放到同一个大“箱子”里——当编译器遇到需要变量捕获的情况时,它会默默地在后台构造一个类型,这个类型包含了每一个闭包所捕获的变量(包括值类型变量和引用类型变量)作为它的一个公有字段。这样,编译器就可以

轻松愉快地

维护那些在匿名函数或lambda表达式中出现的外部变量了。  

 

更进一步,如果我们使用ILDASM工具查看CreateFunction方法的IL代码,我们会发现编译器压根就没有声明num和str变量。取而代之的是声明了一个类型名和实例名都及其难看的包装对象。这个玩意儿就是我们上面所说的那个被编译器默默生成,保存了所有捕获变量的引用的对象。我们还可以看到,在

 

 

CreateFunction方法

 

 

C#源代码内所有对str和num的操作,在IL中都被转换成了对包装对象的同名公有成员的操作。顺便说一句,就连我们构造的那个lambda表达式“() => str + num”现在都被编译器转换成了这个包装对象的一个方法!

posted @ 2011-05-22 14:26  Roy Cheng  阅读(3436)  评论(6编辑  收藏