前言

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. php使用 memcache 来存储 session 方法总结

    设置session用memcache来存储 方法I: 在 php.ini 中全局设置 session.save_handler = memcache session.save_path = " ...

  2. Choose Concurrency-Friendly Data Structures

    What is a high-performance data structure? To answer that question, we're used to applying normal co ...

  3. 【iCore3 双核心板】例程十九:USBD_MSC实验——虚拟U盘

    实验指导书及代码包下载: http://pan.baidu.com/s/1i4eNbQd iCore3 购买链接: https://item.taobao.com/item.htm?id=524229 ...

  4. [troubleshoot][archlinux][X] GPU HANG

    前言:如下内容已经是在hang完大概半个多月后了,当时想写,一直没过来写,写blog果然也是已经花费时间的事情. 最近一直在休假,电脑的使用频率也不多.后来还是为了生活,不情愿的去开始上班了,上班的第 ...

  5. AFN 2.6 code报错总结

    1. 错误打印  code=-1016 filed: text/html 错误原因:AFN默认不能解析请求回来的text/html数据 解决办法: AFN3.0的请看这里 AFHTTPSessionM ...

  6. tomcat session cluster

    Session的生命周期 以前在学习的时候没怎么注意,今天又回过头来仔细研究研究了一下Session的生命周期. Session存储在服务器端,一般为了防止在服务器的内存中(为了高速存取),Sessi ...

  7. C#异步编程简单的运用

    当一个方法中有很多复杂的操作的时候就可以使用异步编程. 假如说这一个方法中有很多复杂的操作,把每一个复杂的操作放到一个异步方法中. 原来程序需要这些方法,上一个执行完成之后,才能执行下一个操作. 但是 ...

  8. Android EditText组件drawableLeft属性设置的图片和hint设置的文字之间的距离

    有的时候,我们需要在文本框里放置icon图片,并且设置默认提示文字的时候,需要设置两者之间的间距,如下图: 这里想设置的就是之前的手机icon和”请输入手机号“之间的距离,则可是使用以下的方式: &l ...

  9. Eclipse安装maven插件报错

    Eclipse安装maven插件,报错信息如下: Cannot complete the install because one or more required items could not be ...

  10. 获取图片中感兴趣区域的信息(Matlab实现)

    内容提要 如果一幅图中只有一小部分图像你感兴趣(你想研究的部分),那么截图工具就可以了,但是如果你想知道这个区域在原图像中的坐标位置呢? 这可是截图工具所办不到的,前段时间我就需要这个功能,于是将其用 ...