C# 从CIL代码了解委托,匿名方法,Lambda 表达式和闭包本质

前言

C# 3.0 引入了 Lambda 表达式,程序员们很快就开始习惯并爱上这种简洁并极具表达力的函数式编程特性。程序员

本着知其然,还要知其因此然的学习态度,笔者不由想到了几个问题。编程

(1)匿名函数(匿名方法和Lambda 表达式统称)如何实现的?闭包

(2)Lambda表达式除了书写格式以外还有什么特别的地方呢?ide

(3)匿名函数是如何捕获变量的?函数式编程

(4)神奇的闭包是如何实现的?函数

本文将基于CIL代码探寻Lambda表达式和匿名方法的本质。学习

笔者一直认为委托能够说是C#最重要的元素之一,有不少东西都是基于委托实现的,如事件。关于委托的详细说明已经有不少好的资料,本文就再也不墨迹,有兴趣的朋友能够去MSDN看看http://msdn.microsoft.com/zh-cn/library/900fyy8e(v=VS.80).aspxspa

目录

三种实现委托的方法3d

从CIL代码比较匿名方法和Lambda表达式区别指针

从CIL代码研究带有参数的委托

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

正文

1.三种实现委托的方法

1.1下面先从一个简单的例子比较命名方法,匿名方法和Lambda 表达式三种实现委托的方法

(1)申明一个委托,固然这只是一个最简单的委托,没有参数和返回值,因此可使用Action 委托

delegate void DelegateTest();
View Code

(2)建立一个静态方法,以做为参数实例化委托

static void DelegateTestMethod()
{
    System.Console.WriteLine("命名方式");
}
View Code

(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();
View Code

输出

命名方式

匿名方法

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();
View Code

代码2采用Lambda 表达式

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

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() = ( 01 00 00 00 ) 
  // 代码大小       13 (0xd)
  .maxstack  8
  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  3
  .locals init ([0] 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.0
//从计算堆栈的顶部弹出当前值并将其存储到指定索引处的局部变量列表中。
  IL_0021:  ldloc.0
//将指定索引处的局部变量加载到计算堆栈上。
  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);
View Code

(2)将主函数改成

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

输出结果

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() = ( 01 00 00 00 ) 
  // 代码大小       9 (0x9)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldarg.0
  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  3
  .locals init ([0] 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.0
  IL_0021:  ldloc.0
  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);
View Code

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

int t = 10;

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

delTest(100);
View Code

输出结果

110

4.2查看CIL代码

分析类结构

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

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // 代码大小       45 (0x2d)
  .maxstack  3
  .locals init ([0] class ClosureTest.Program/DelTest delTest,
           [1] class ClosureTest.Program/'<>c__DisplayClass1' 'CS$<>8__locals2')
  IL_0000:  newobj     instance void ClosureTest.Program/'<>c__DisplayClass1'::.ctor()
//建立一个对象
  IL_0005:  stloc.1
//计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中。
  IL_0006:  nop
  IL_0007:  ldloc.1
//将索引 1 处的局部变量加载到计算堆栈上。
  IL_0008:  ldc.i4.s   10
//将提供的 int8 值做为 int32 推送到计算堆栈上(短格式)。
  IL_000a:  stfld      int32 ClosureTest.Program/'<>c__DisplayClass1'::t
//用新值替换在对象引用或指针的字段中存储的值。
  IL_000f:  ldloc.1
//将索引 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
//计算堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量列表中。
  IL_001c:  ldloc.0
//将索引 0 处的局部变量加载到计算堆栈上。
  IL_001d:  ldc.i4.s   100
//将提供的 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  8
  IL_0000:  nop
  IL_0001:  ldstr      "{0}"
//推送对元数据中存储的字符串的新对象引用。
  IL_0006:  ldarg.0
//将索引为 0 的参数加载到计算堆栈上。
  IL_0007:  ldfld      int32 ClosureTest.Program/'<>c__DisplayClass1'::t
//查找对象中其引用当前位于计算堆栈的字段的值。
  IL_000c:  ldarg.1
//将索引为 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以前的范畴。

相关文章
相关标签/搜索