C# 从CIL代码了解委托,匿名方法,Lambda 表达式和闭包本质
前言
C# 3.0 引入了 Lambda 表达式,程序员们很快就开始习惯并爱上这种简洁并极具表达力的函数式编程特性。
本着知其然,还要知其所以然的学习态度,笔者不禁想到了几个问题。
(1)匿名函数(匿名方法和Lambda 表达式统称)如何实现的?
(2)Lambda表达式除了书写格式之外还有什么特别的地方呢?
(3)匿名函数是如何捕获变量的?
(4)神奇的闭包是如何实现的?
本文将基于CIL代码探寻Lambda表达式和匿名方法的本质。
笔者一直认为委托可以说是C#最重要的元素之一,有很多东西都是基于委托实现的,如事件。关于委托的详细说明已经有很多好的资料,本文就不再墨迹,有兴趣的朋友可以去MSDN看看http://msdn.microsoft.com/zh-cn/library/900fyy8e(v=VS.80).aspx
目录
三种实现委托的方法
从CIL代码比较匿名方法和Lambda表达式区别
从CIL代码研究带有参数的委托
从CIL代码研究匿名函数捕获变量和闭包的实质
正文
1.三种实现委托的方法
1.1下面先从一个简单的例子比较命名方法,匿名方法和Lambda 表达式三种实现委托的方法
(1)申明一个委托,当然这只是一个最简单的委托,没有参数和返回值,所以可以使用Action 委托
delegate void DelegateTest();
(2)创建一个静态方法,以作为参数实例化委托
static void DelegateTestMethod() { System.Console.WriteLine("命名方式"); }
(3)在主函数中添加代码
//命名方式 DelegateTest dt0 = new DelegateTest(DelegateTestMethod); //匿名方法 DelegateTest dt1 = delegate() { System.Console.WriteLine("匿名方法"); }; //Lambda 表达式 DelegateTest dt2 = ()=> { System.Console.WriteLine("Lambda 表达式"); }; dt0(); dt1(); dt2(); System.Console.ReadLine();
输出
命名方式
匿名方法
Lambda 表达式
1.2说明
通过这个例子可以看出,三种方法中命名方式是最麻烦的,代码也很臃肿,而匿名方法和Lambda 表达式则直接简洁很多。这个例子只是实现最简单的委托,没有参数和返回值,事实上Lambda 表达式较匿名方法更直接,更具有表达力。本文就不详细介绍Lambda表示式了,可以在MSDN上详细了解http://msdn.microsoft.com/zh-cn/library/bb397687.aspx那么Lambda表达式除了书写方式和匿名方法不同之外,还有什么不一样的地方吗?众所周知,.Net工程编译生成的输出文件是程序集,而程序集中的代码并不是可以直接运行的本机代码,而是被称为CIL(IL和MSIL都是曾用名,本文采用CIL)的中间语言。
原理图如下:

因此可以通过CIL代码研究C#语言的实现方式。(本文采用ildasm.exe查看CIL代码)
2.从CIL代码比较匿名方法和Lambda表达式区别
2.1C#代码
为了便于研究,将之前的例子拆分为两个不同的程序,唯一区别在于主函数
代码1采用匿名方法
View Code代码2采用Lambda 表达式
View Code2.2查看代码1程序集CIL代码
用ildasm.exe查看代码1生成程序集的CIL代码

可以分析出CIL中类结构:

静态函数CIL代码
CIL代码主函数
CIL代码
2.3查看代码2程序集CIL代码
用ildasm.exe查看代码2生成程序集的CIL代码

通过比较发现和代码1生成程序集的CIL代码完全一样。
2.4分析
可以清楚的发现在CIL代码中有一个静态的方法<Main>b__0,其内容就是匿名方法和Lambda 表达式语句块中的内容。在主函数中通过<Main>b__0实例委托,并调用。
2.5结论
无论是用匿名方法还是Lambda 表达式实现的委托,其本质都是完全相同。他们的原理都是在C#语言编译过程中,创建了一个静态的方法实例委托的对象。也就是说匿名方法和Lambda 表达式在CIL中其实都是采用命名方法实例化委托。
C#在通过匿名函数实现委托时,需要做以下步骤
(1)一个静态的方法(<Main>b__0),用以实现匿名函数语句块内容
(2)用方法(<Main>b__0)实例化委托
匿名函数在CIL代码中实现的原理图

3.从CIL代码研究带有参数的委托
3.1C#代码
为了便于研究采用匿名方法实现委托的方式,将代码改为:
(1)将委托改为
View Code(2)将主函数改为
View Code输出结果
Just for test
3.2查看CIL代码

静态函数
CIL代码主函数
CIL代码
3.3分析
可以看出与上一节的例子唯一不同的是CIL代码中生成的静态函数需要传递一个string对象作为参数。
3.4结论
委托是否带有参数对于C#实现基本没有影响。
4.从CIL代码研究匿名函数捕获变量和闭包的实质
匿名函数不同于命名方法,可以访问它门外围作用域的局部变量和环境。本文采用了一个例子说明匿名函数(Lambda 表达式)可以捕获外围变量。而只要匿名函数有效,即使变量已经离开了作用域,这个变量的生命周期也会随之扩展。这个现象被称为闭包。

4.1C#代码
代码如下:
(1)定义一个委托
View Code(2)在主函数中添加中添加代码
View Code输出结果
110
4.2查看CIL代码

分析类结构

分析Program::Main方法(主函数)

CIL代码分析<>c__DisplayClass1::<Main>b__0方法

CIL代码
4.3分析
可以看到与之前的例子不同,CIL代码中创建了一个叫做<>c__DisplayClass1的类,在类中有一个字段public int32 t,和方法<Main>b__0,分别对应要捕获的变量和匿名函数的语句块。
从主函数可以分析出流程
(1)创建一个<>c__DisplayClass1实例对象
(2)将<>c__DisplayClass1实例对象的字段t赋值为10
(3)创建一个DelTest委托类的实例对象,将<>c__DisplayClass1实例对象的<Main>b__0方法传递给构造函数
(4)调用DelTest委托,并将100作为参数
这时就不难理解闭包现象了,因为C#其实用类的字段来捕获变量(无论值类型还是引用类型),所其作用域当然会随着匿名函数的生存周期而延长。
4.4结论
C#在通过匿名函数实现需要捕获变量的委托时,需要做以下步骤
(1)创建一个类(<>c__DisplayClass1)
(2)在类中根据将要捕获的变量创建对应的字段(public int32 t)
(3)在类中创建一个方法(<Main>b__0),用以实现匿名函数语句块内容
(4)创建类(<>c__DisplayClass1)的对象,并用其方法(<Main>b__0)实例化委托
闭包现象则是因为步骤(2),捕获变量的实现方式所带来的附加产物。
需要捕获变量的匿名函数在CIL代码中实现原理图

结论
C#在实现匿名函数(匿名方法和Lambda 表达式),是通过隐式的创建一个静态方法或者类(需要捕获变量时),然后通过命名方式创建委托。
本文到这里笔者已经完成了对匿名方法,Lambda 表达式和闭包的探索, 明白了这些都是C#为了方便用户编写代码而准备的“语法糖”,其本质并未超出.Net之前的范畴。

浙公网安备 33010602011771号