前言

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采用匿名方法

//匿名方法
DelegateTest dt = delegate()
{
System.Console.WriteLine("Just for test");
};
dt();

代码2采用Lambda 表达式

//Lambda 表达式
DelegateTest dt = () =>
{
System.Console.WriteLine("Just for test");
};
dt();
 

2.2查看代码1程序集CIL代码

用ildasm.exe查看代码1生成程序集的CIL代码

可以分析出CIL中类结构:

静态函数CIL代码

.method private hidebysig static void  '<Main>b__0'() cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( )
// 代码大小 13 (0xd)
.maxstack
IL_0000: nop
IL_0001: ldstr "Just for test"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: ret
} // end of method Program::'<Main>b__0'

CIL代码

主函数

.method private hidebysig static void  Main(string[] args) cil managed
{
.entrypoint
// 代码大小 47 (0x2f)
.maxstack
.locals init ([] class DelegateTestDemo.Program/DelegateTest dt)
IL_0000: nop
IL_0001: ldsfld class DelegateTestDemo.Program/DelegateTest DelegateTestDemo.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
//将静态字段的值推送到计算堆栈上。
IL_0006: brtrue.s IL_001b
//如果 value 为 true、非空或非零,则将控制转移到目标指令(短格式)。
IL_0008: ldnull
//将空引用(O 类型)推送到计算堆栈上
IL_0009: ldftn void DelegateTestDemo.Program::'<Main>b__0'()
//将指向实现特定方法的本机代码的非托管指针(natural int 类型)推送到计算堆栈上。
IL_000f: newobj instance void DelegateTestDemo.Program/DelegateTest::.ctor(object, native int)
//创建一个值类型的新对象或新实例,并将对象引用(O 类型)推送到计算堆栈上。
IL_0014: stsfld class DelegateTestDemo.Program/DelegateTest DelegateTestDemo.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
//用来自计算堆栈的值替换静态字段的值。
IL_0019: br.s IL_001b
//无条件地将控制转移到目标指令(短格式)。
IL_001b: ldsfld class DelegateTestDemo.Program/DelegateTest DelegateTestDemo.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
//将静态字段的值推送到计算堆栈上。
IL_0020: stloc.
//从计算堆栈的顶部弹出当前值并将其存储到指定索引处的局部变量列表中。
IL_0021: ldloc.
//将指定索引处的局部变量加载到计算堆栈上。
IL_0022: callvirt instance void DelegateTestDemo.Program/DelegateTest::Invoke()
//对对象调用后期绑定方法,并且将返回值推送到计算堆栈上。
IL_0027: nop
IL_0028: call string [mscorlib]System.Console::ReadLine()
//调用由传递的方法说明符指示的方法。
IL_002d: pop
//移除当前位于计算堆栈顶部的值。
IL_002e: ret
//从当前方法返回,并将返回值(如果存在)从调用方的计算堆栈推送到被调用方的计算堆栈上。
} // end of method Program::Main

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)将委托改为

delegate void DelegateTest(string msg);

(2)将主函数改为

DelegateTest dt = delegate(string msg)
{
System.Console.WriteLine(msg);
};
dt("Just for test");

输出结果

Just for test

3.2查看CIL代码

静态函数

.method private hidebysig static void  '<Main>b__0'(string msg) cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( )
// 代码大小 9 (0x9)
.maxstack
IL_0000: nop
IL_0001: ldarg.
IL_0002: call void [mscorlib]System.Console::WriteLine(string)
IL_0007: nop
IL_0008: ret
} // end of method Program::'<Main>b__0'

CIL代码

主函数

.method private hidebysig static void  Main(string[] args) cil managed
{
.entrypoint
// 代码大小 52 (0x34)
.maxstack
.locals init ([] class DelegateTestDemo.Program/DelegateTest dt)
IL_0000: nop
IL_0001: ldsfld class DelegateTestDemo.Program/DelegateTest DelegateTestDemo.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0006: brtrue.s IL_001b
IL_0008: ldnull
IL_0009: ldftn void DelegateTestDemo.Program::'<Main>b__0'(string)
IL_000f: newobj instance void DelegateTestDemo.Program/DelegateTest::.ctor(object,
native int)
IL_0014: stsfld class DelegateTestDemo.Program/DelegateTest DelegateTestDemo.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0019: br.s IL_001b
IL_001b: ldsfld class DelegateTestDemo.Program/DelegateTest DelegateTestDemo.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0020: stloc.
IL_0021: ldloc.
IL_0022: ldstr "Just for test"
IL_0027: callvirt instance void DelegateTestDemo.Program/DelegateTest::Invoke(string)
IL_002c: nop
IL_002d: call string [mscorlib]System.Console::ReadLine()
IL_0032: pop
IL_0033: ret
} // end of method Program::Main

CIL代码

3.3分析

可以看出与上一节的例子唯一不同的是CIL代码中生成的静态函数需要传递一个string对象作为参数。

3.4结论

委托是否带有参数对于C#实现基本没有影响。

4.从CIL代码研究匿名函数捕获变量和闭包的实质

匿名函数不同于命名方法,可以访问它门外围作用域的局部变量和环境。本文采用了一个例子说明匿名函数(Lambda 表达式)可以捕获外围变量。而只要匿名函数有效,即使变量已经离开了作用域,这个变量的生命周期也会随之扩展。这个现象被称为闭包。

4.1C#代码

代码如下:

(1)定义一个委托

delegate void DelTest(int n);

(2)在主函数中添加中添加代码

int t = ;

DelTest delTest = (n) =>
{
System.Console.WriteLine("{0}", t + n);
}; delTest();

输出结果

110

4.2查看CIL代码

分析类结构

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

.method private hidebysig static void  Main(string[] args) cil managed
{
.entrypoint
// 代码大小 45 (0x2d)
.maxstack
.locals init ([] class ClosureTest.Program/DelTest delTest,
[] class ClosureTest.Program/'<>c__DisplayClass1' 'CS$<>8__locals2')
IL_0000: newobj instance void ClosureTest.Program/'<>c__DisplayClass1'::.ctor()
//创建一个对象
IL_0005: stloc.
//计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中。
IL_0006: nop
IL_0007: ldloc.
//将索引 1 处的局部变量加载到计算堆栈上。
IL_0008: ldc.i4.s
//将提供的 int8 值作为 int32 推送到计算堆栈上(短格式)。
IL_000a: stfld int32 ClosureTest.Program/'<>c__DisplayClass1'::t
//用新值替换在对象引用或指针的字段中存储的值。
IL_000f: ldloc.
//将索引 1 处的局部变量加载到计算堆栈上。
IL_0010: ldftn instance void ClosureTest.Program/'<>c__DisplayClass1'::'<Main>b__0'(int32)
//将指向实现特定方法的本机代码的非托管指针(natural int 类型)推送到计算堆栈上。
IL_0016: newobj instance void ClosureTest.Program/DelTest::.ctor(object,
native int)
//创建一个对象
IL_001b: stloc.
//计算堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量列表中。
IL_001c: ldloc.
//将索引 0 处的局部变量加载到计算堆栈上。
IL_001d: ldc.i4.s
//将提供的 int8 值作为 int32 推送到计算堆栈上(短格式)。
IL_001f: callvirt instance void ClosureTest.Program/DelTest::Invoke(int32)
//对对象调用后期绑定方法,并且将返回值推送到计算堆栈上。
IL_0024: nop
IL_0025: call string [mscorlib]System.Console::ReadLine()
IL_002a: pop
IL_002b: nop
IL_002c: ret
} // end of method Program::Main

CIL代码

分析<>c__DisplayClass1::<Main>b__0方法

.method public hidebysig instance void  '<Main>b__0'(int32 n) cil managed
{
// 代码大小 26 (0x1a)
.maxstack
IL_0000: nop
IL_0001: ldstr "{0}"
//推送对元数据中存储的字符串的新对象引用。
IL_0006: ldarg.
//将索引为 0 的参数加载到计算堆栈上。
IL_0007: ldfld int32 ClosureTest.Program/'<>c__DisplayClass1'::t
//查找对象中其引用当前位于计算堆栈的字段的值。
IL_000c: ldarg.
//将索引为 1 的参数加载到计算堆栈上。
IL_000d: add
//将两个值相加并将结果推送到计算堆栈上。
IL_000e: box [mscorlib]System.Int32
//将值类转换为对象引用(O 类型)。
IL_0013: call void [mscorlib]System.Console::WriteLine(string,
object)
//调用由传递的方法说明符指示的方法。
IL_0018: nop
IL_0019: ret
} // end of method '<>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之前的范畴。

C# 从CIL代码了解委托,匿名方法,Lambda 表达式和闭包本质的更多相关文章

  1. 委托-异步调用-泛型委托-匿名方法-Lambda表达式-事件【转】

    1. 委托 From: http://www.cnblogs.com/daxnet/archive/2008/11/08/1687014.html 类是对象的抽象,而委托则可以看成是函数的抽象.一个委 ...

  2. C#多线程+委托+匿名方法+Lambda表达式

    线程 下面是百度写的: 定义英文:Thread每个正在系统上运行的程序都是一个进程.每个进程包含一到多个线程.进程也可能是整个程序或者是部分程序的动态执行.线程是一组指令的集合,或者是程序的特殊段,它 ...

  3. C# delegate event func action 匿名方法 lambda表达式

    delegate event action func 匿名方法 lambda表达式 delegate类似c++的函数指针,但是是类型安全的,可以指向多个函数, public delegate void ...

  4. 18、(番外)匿名方法+lambda表达式

    概念了解: 1.什么是匿名委托(匿名方法的简单介绍.为什么要用匿名方法) 2.匿名方法的[拉姆达表达式]方法定义 3.匿名方法的调用(匿名方法的参数传递.使用过程中需要注意什么) 什么是匿名方法? 匿 ...

  5. 匿名函数 =匿名方法+ lambda 表达式

    匿名函数的定义和用途 匿名函数是一个"内联"语句或表达式,可在需要委托类型的任何地方使用. 可以使用匿名函数来初始化命名委托[无需取名字的委托],或传递命名委托(而不是命名委托类型 ...

  6. C#委托总结-匿名方法&Lambda表达式

    1,匿名方法 匿名方法可以在声明委托变量时初始化表达式,语法如下 之前写过这么一段代码: delegate void MyDel(string value); class Program { void ...

  7. 委托delegate 泛型委托action<> 返回值泛型委托Func<> 匿名方法 lambda表达式 的理解

    1.使用简单委托 namespace 简单委托 { class Program { //委托方法签名 delegate void MyBookDel(int a); //定义委托 static MyB ...

  8. (28)C#委托,匿名函数,lambda表达式,事件

    一.委托 委托是一种用于封装命名和匿名方法的引用类型. 把方法当参数,传给另一个方法(这么说好理解,但实际上方法不能当参数,传入的是委托类型),委托是一种引用类型,委托里包含很多方法的引用 创建的方法 ...

  9. lambda 委托 匿名方法

    委托: delegate是C#中的一种类型,它实际上是一个能够持有对某个方法的引用的类.与其它的类不同,delegate类能够拥有一个签名(signature),并且它只能持有与它的签名相匹配的方法的 ...

随机推荐

  1. BizTalk开发系列(三十八)微软BizTalk Server定价和许可[解读]

    做BizTalk的项目一段时间了,但是对BizTalk的价格和许可还不是很了解.给客户设计解决方案时大部分产品都是直接按照企业版的功能来设计,很 少考虑到价格和许可方面的因素,以为这个不是我们的事情或 ...

  2. Android课程---表格布局TableLayout

    特别注意:由于表格布局继承自线性布局,因此并不显示表格线 示例代码: <?xml version="1.0" encoding="utf-8"?> ...

  3. A trip through the Graphics Pipeline 2011_13 Compute Shaders, UAV, atomic, structured buffer

    Welcome back to what’s going to be the last “official” part of this series – I’ll do more GPU-relate ...

  4. [转载] Windows + IIS + PHP 配置

    资源下载: 下载windwos版本的PHP:http://windows.php.net/download/ (我下载的是PHP5.4.9_VC9 x86 Non Thread Safe,下载地址:h ...

  5. NEC学习 ---- 布局 -三列, 左右定宽,中间自适应

    ---恢复内容开始--- 这个布局很牛掰, 我觉得学习价值很大. 通过这个的学习, 我发现, 能将简单的事情做好, 就距离成功不远了. 其实布局就是利用所学知识, 活用. 在没看这个之前, 发现自己的 ...

  6. AJAX原理及XMLHttpRequest对象分析

    今天的主题是前端都了解的AJAX,但其中都有哪些知识点,还需要深入分析. 首先揭示AJAX的字面意思,Asynchronous Javascript And XML,通俗点就是“异步Javascrip ...

  7. javaWeb中servlet开发(4)——servlet跳转

    servlet跳转 1.跳转类型 客户端跳转:跳转后地址栏改变,无法传递request范围内属性,是在所有的操作都执行完毕之后才发生跳转的操作,跳转语法是,response.sendRedict() ...

  8. 弱引用?强引用?未持有?额滴神啊-- Swift 引用计数指导

    ARC ARC 苹果版本的自动内存管理的编译时间特性.它代表了自动引用计数(Automatic Reference Counting).也就是对于一个对象来说,只有在引用计数为0的情况下内存才会被释放 ...

  9. 加载UI

    weak情况 1 2 3 4 @property (weak,nonatomic) UILabel *nameLabel;   UILabel *nameLabel = [[UILabel alloc ...

  10. Django:使用PyCharm创建django项目并发布到apache2.4

    环境: python2.7 x64 win7 x64 django (通过pycharm创建时自动安装)版本:1.10.2 apache:2.4 x64 下载pycharm个人版非社区版本并激活 起初 ...