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();
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 8IL_0000: nopIL_0001: ldstr "Just for test"IL_0006: call void [mscorlib]System.Console::WriteLine(string)IL_000b: nopIL_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: nopIL_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: nopIL_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 8IL_0000: nopIL_0001: ldarg.0IL_0002: call void [mscorlib]System.Console::WriteLine(string)IL_0007: nopIL_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: nopIL_0001: ldsfld class DelegateTestDemo.Program/DelegateTest DelegateTestDemo.Program::'CS$<>9__CachedAnonymousMethodDelegate1'IL_0006: brtrue.s IL_001bIL_0008: ldnullIL_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_001bIL_001b: ldsfld class DelegateTestDemo.Program/DelegateTest DelegateTestDemo.Program::'CS$<>9__CachedAnonymousMethodDelegate1'IL_0020: stloc.0IL_0021: ldloc.0IL_0022: ldstr "Just for test"IL_0027: callvirt instance void DelegateTestDemo.Program/DelegateTest::Invoke(string)IL_002c: nopIL_002d: call string [mscorlib]System.Console::ReadLine()IL_0032: popIL_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: nopIL_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: nopIL_0025: call string [mscorlib]System.Console::ReadLine()IL_002a: popIL_002b: nopIL_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 8IL_0000: nopIL_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: nopIL_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之前的范畴。
转载于:https://www.cnblogs.com/max198727/p/3436220.html
C# 从CIL代码了解委托,匿名方法,Lambda 表达式和闭包本质相关推荐
- java 匿名委托_委托,匿名方法,λ 表达式
1.委托:委托本质上就是函数指针,但由于指针过于灵活,因此在很多语言中都采用了更加安全的替代类型,比如Delphi的对象方法和C#的委托.委托使得方法可以做为参数进行传递,极大的方便了程序的处理(事件 ...
- C++11新特性中的匿名函数Lambda表达式的汇编实现分析(二)
2019独角兽企业重金招聘Python工程师标准>>> C++11新特性中的匿名函数Lambda表达式的汇编实现分析(一) 首先,让我们来看看以&方式进行变量捕获,同样没有参 ...
- 【Kotlin】Kotlin 函数总结 ( 具名函数 | 匿名函数 | Lambda 表达式 | 闭包 | 内联函数 | 函数引用 )
文章目录 一.函数头声明 二.函数参数 1.默认参数值 2.具名参数 三.Unit 函数 四.TODO 函数抛出异常返回 Nothing 类型 五.反引号函数名 六.匿名函数 七.匿名函数的函数类型 ...
- 【java】代码的简化(匿名内部类/Lambda表达式)
在学习java swing基础的过程中,可能会遇到多个事件的监听,当监听一个事件就会new一个监听事件的类,这样未免过于麻烦,我们可以采用匿名内部类的方法进行监听 示例代码如下: button.add ...
- 14_python基础—匿名函数lambda表达式
文章目录 一.lambda 表达式 1.1 lambda的应用场景 1.2 lambda语法 快速入门 1.3 示例:计算a + b 1.3.1 函数实现 1.3.2 lambda实现 1.4 lam ...
- C#中的委托和Java中的“委托”(Java8 lambda表达式)
Java8中新增了有关对lambda表达式的支持,其本质是一种编译阶段的语法糖,效果是双向的,代码看似简洁的前提下牺牲了部分可读性. 我们可以尝试实现一些有意思的东西,先来看段C#委托(delegat ...
- python匿名函数的作用_Python匿名函数 Lambda表达式作用
在Python这门优美的编程语言中,支持一种有趣的语法格式(表达式),可以让我们在单行内创建一个最小的函数-python lambda匿名函数. 据说是借鉴了Lisp语言中lambda表达式,它可以使 ...
- Scala匿名函数Lambda表达式详解
1 前言 大家好,我是楚生辉,在未来的日子里我们一起来学习大数据相关的技术,一起努力奋斗,遇见更好的自己! 本文详细的介绍了scala的匿名函数,有需要的小伙伴可以学习~ Scala 中定义匿名函数的 ...
- C++ [](){} 匿名函数 lambda表达式
C++ [](){}_龍的传人-CSDN博客_[]() c++ lambda函数能够捕获lambda函数外的具有自动存储时期的变量.函数体与这些变量的集合合起来叫闭包.lambda函数能够捕获lamb ...
最新文章
- php set medias,laravel5.1 -- Integrate FileManager and CKeditor into laravel
- 当手机淘宝遇见海尔电商
- VUE项目中使用this.$forceUpdate();解决页面v-for中修改item属性值后页面v-if不改变的问题
- 训练不出结果_工业设计师如何训练自己的设计思维? 问答
- 科大星云诗社动态20210821
- R语言处理1975-2011年的人口信息
- IBM Java垃圾回收
- 什么是算术运算和逻辑运算_8086微处理器的算术和逻辑运算
- python编程基础张勇答案_Python程序开发、编程基础阶段试题及答案
- java xframeoptions,Header:X-Frame-Options开启与关闭方法
- 《手把手教你》系列基础篇之1-python+ selenium自动化测试-环境搭建(详细)
- sqlyog-mysql_SQLyog
- 深入Managed DirectX9
- JSP之【include】指令
- jsoup爬虫实战详解之新浪
- 梨花众创 - PacketView工业控制协议分析系统 简介
- Alien Skin Exposure新版X8功能介绍
- C/C++牛客网刷题练习之翻转链表篇
- 如何查看iOS版本?
- HTML+CSS网页设计期末课程大作业 【茶叶文化网站设计题材】web前端开发技术 web课程设计 网页规划与设计