C# 从CIL代码了解委托,匿名方法,Lambda 表达式和闭包本质
前言
C# 3.0 引入了 Lambda 表達(dá)式,程序員們很快就開(kāi)始習(xí)慣并愛(ài)上這種簡(jiǎn)潔并極具表達(dá)力的函數(shù)式編程特性。
本著知其然,還要知其所以然的學(xué)習(xí)態(tài)度,筆者不禁想到了幾個(gè)問(wèn)題。
(1)匿名函數(shù)(匿名方法和Lambda 表達(dá)式統(tǒng)稱(chēng))如何實(shí)現(xiàn)的?
(2)Lambda表達(dá)式除了書(shū)寫(xiě)格式之外還有什么特別的地方呢?
(3)匿名函數(shù)是如何捕獲變量的?
(4)神奇的閉包是如何實(shí)現(xiàn)的?
本文將基于CIL代碼探尋Lambda表達(dá)式和匿名方法的本質(zhì)。
筆者一直認(rèn)為委托可以說(shuō)是C#最重要的元素之一,有很多東西都是基于委托實(shí)現(xiàn)的,如事件。關(guān)于委托的詳細(xì)說(shuō)明已經(jīng)有很多好的資料,本文就不再墨跡,有興趣的朋友可以去MSDN看看http://msdn.microsoft.com/zh-cn/library/900fyy8e(v=VS.80).aspx
目錄
三種實(shí)現(xiàn)委托的方法
從CIL代碼比較匿名方法和Lambda表達(dá)式區(qū)別
從CIL代碼研究帶有參數(shù)的委托
從CIL代碼研究匿名函數(shù)捕獲變量和閉包的實(shí)質(zhì)
正文
1.三種實(shí)現(xiàn)委托的方法
1.1下面先從一個(gè)簡(jiǎn)單的例子比較命名方法,匿名方法和Lambda 表達(dá)式三種實(shí)現(xiàn)委托的方法
(1)申明一個(gè)委托,當(dāng)然這只是一個(gè)最簡(jiǎn)單的委托,沒(méi)有參數(shù)和返回值,所以可以使用Action 委托
delegate void DelegateTest(); View Code(2)創(chuàng)建一個(gè)靜態(tài)方法,以作為參數(shù)實(shí)例化委托
static void DelegateTestMethod() {System.Console.WriteLine("命名方式"); } View Code(3)在主函數(shù)中添加代碼
//命名方式 DelegateTest dt0 = new DelegateTest(DelegateTestMethod);//匿名方法 DelegateTest dt1 = delegate() {System.Console.WriteLine("匿名方法"); };//Lambda 表達(dá)式 DelegateTest dt2 = ()=> {System.Console.WriteLine("Lambda 表達(dá)式"); };dt0(); dt1(); dt2();System.Console.ReadLine(); View Code輸出
命名方式
匿名方法
Lambda 表達(dá)式
1.2說(shuō)明
通過(guò)這個(gè)例子可以看出,三種方法中命名方式是最麻煩的,代碼也很臃腫,而匿名方法和Lambda 表達(dá)式則直接簡(jiǎn)潔很多。這個(gè)例子只是實(shí)現(xiàn)最簡(jiǎn)單的委托,沒(méi)有參數(shù)和返回值,事實(shí)上Lambda 表達(dá)式較匿名方法更直接,更具有表達(dá)力。本文就不詳細(xì)介紹Lambda表示式了,可以在MSDN上詳細(xì)了解http://msdn.microsoft.com/zh-cn/library/bb397687.aspx那么Lambda表達(dá)式除了書(shū)寫(xiě)方式和匿名方法不同之外,還有什么不一樣的地方嗎?眾所周知,.Net工程編譯生成的輸出文件是程序集,而程序集中的代碼并不是可以直接運(yùn)行的本機(jī)代碼,而是被稱(chēng)為CIL(IL和MSIL都是曾用名,本文采用CIL)的中間語(yǔ)言。
原理圖如下:
???
因此可以通過(guò)CIL代碼研究C#語(yǔ)言的實(shí)現(xiàn)方式。(本文采用ildasm.exe查看CIL代碼)
2.從CIL代碼比較匿名方法和Lambda表達(dá)式區(qū)別
2.1C#代碼
為了便于研究,將之前的例子拆分為兩個(gè)不同的程序,唯一區(qū)別在于主函數(shù)
代碼1采用匿名方法
//匿名方法 DelegateTest dt = delegate() {System.Console.WriteLine("Just for test"); }; dt(); View Code代碼2采用Lambda 表達(dá)式
//Lambda 表達(dá)式 DelegateTest dt = () => {System.Console.WriteLine("Just for test"); }; dt(); View Code ?2.2查看代碼1程序集CIL代碼
用ildasm.exe查看代碼1生成程序集的CIL代碼
可以分析出CIL中類(lèi)結(jié)構(gòu):
靜態(tài)函數(shù)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代碼主函數(shù)
.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' //將靜態(tài)字段的值推送到計(jì)算堆棧上。 IL_0006: brtrue.s IL_001b //如果 value 為 true、非空或非零,則將控制轉(zhuǎn)移到目標(biāo)指令(短格式)。 IL_0008: ldnull //將空引用(O 類(lèi)型)推送到計(jì)算堆棧上IL_0009: ldftn void DelegateTestDemo.Program::'<Main>b__0'() //將指向?qū)崿F(xiàn)特定方法的本機(jī)代碼的非托管指針(natural int 類(lèi)型)推送到計(jì)算堆棧上。IL_000f: newobj instance void DelegateTestDemo.Program/DelegateTest::.ctor(object, native int) //創(chuàng)建一個(gè)值類(lèi)型的新對(duì)象或新實(shí)例,并將對(duì)象引用(O 類(lèi)型)推送到計(jì)算堆棧上。IL_0014: stsfld class DelegateTestDemo.Program/DelegateTest DelegateTestDemo.Program::'CS$<>9__CachedAnonymousMethodDelegate1' //用來(lái)自計(jì)算堆棧的值替換靜態(tài)字段的值。 IL_0019: br.s IL_001b //無(wú)條件地將控制轉(zhuǎn)移到目標(biāo)指令(短格式)。IL_001b: ldsfld class DelegateTestDemo.Program/DelegateTest DelegateTestDemo.Program::'CS$<>9__CachedAnonymousMethodDelegate1' //將靜態(tài)字段的值推送到計(jì)算堆棧上。IL_0020: stloc.0 //從計(jì)算堆棧的頂部彈出當(dāng)前值并將其存儲(chǔ)到指定索引處的局部變量列表中。IL_0021: ldloc.0 //將指定索引處的局部變量加載到計(jì)算堆棧上。IL_0022: callvirt instance void DelegateTestDemo.Program/DelegateTest::Invoke() //對(duì)對(duì)象調(diào)用后期綁定方法,并且將返回值推送到計(jì)算堆棧上。 IL_0027: nopIL_0028: call string [mscorlib]System.Console::ReadLine() //調(diào)用由傳遞的方法說(shuō)明符指示的方法。 IL_002d: pop //移除當(dāng)前位于計(jì)算堆棧頂部的值。 IL_002e: ret //從當(dāng)前方法返回,并將返回值(如果存在)從調(diào)用方的計(jì)算堆棧推送到被調(diào)用方的計(jì)算堆棧上。 } // end of method Program::Main CIL代碼?
2.3查看代碼2程序集CIL代碼
用ildasm.exe查看代碼2生成程序集的CIL代碼
通過(guò)比較發(fā)現(xiàn)和代碼1生成程序集的CIL代碼完全一樣。
2.4分析
可以清楚的發(fā)現(xiàn)在CIL代碼中有一個(gè)靜態(tài)的方法<Main>b__0,其內(nèi)容就是匿名方法和Lambda 表達(dá)式語(yǔ)句塊中的內(nèi)容。在主函數(shù)中通過(guò)<Main>b__0實(shí)例委托,并調(diào)用。
2.5結(jié)論
無(wú)論是用匿名方法還是Lambda 表達(dá)式實(shí)現(xiàn)的委托,其本質(zhì)都是完全相同。他們的原理都是在C#語(yǔ)言編譯過(guò)程中,創(chuàng)建了一個(gè)靜態(tài)的方法實(shí)例委托的對(duì)象。也就是說(shuō)匿名方法和Lambda 表達(dá)式在CIL中其實(shí)都是采用命名方法實(shí)例化委托。
C#在通過(guò)匿名函數(shù)實(shí)現(xiàn)委托時(shí),需要做以下步驟
(1)一個(gè)靜態(tài)的方法(<Main>b__0),用以實(shí)現(xiàn)匿名函數(shù)語(yǔ)句塊內(nèi)容
(2)用方法(<Main>b__0)實(shí)例化委托
匿名函數(shù)在CIL代碼中實(shí)現(xiàn)的原理圖
3.從CIL代碼研究帶有參數(shù)的委托
3.1C#代碼
為了便于研究采用匿名方法實(shí)現(xiàn)委托的方式,將代碼改為:
(1)將委托改為
delegate void DelegateTest(string msg); View Code(2)將主函數(shù)改為
DelegateTest dt = delegate(string msg) {System.Console.WriteLine(msg); }; dt("Just for test"); View Code輸出結(jié)果
Just for test
3.2查看CIL代碼
靜態(tài)函數(shù)
.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代碼主函數(shù)
.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分析
可以看出與上一節(jié)的例子唯一不同的是CIL代碼中生成的靜態(tài)函數(shù)需要傳遞一個(gè)string對(duì)象作為參數(shù)。
3.4結(jié)論
委托是否帶有參數(shù)對(duì)于C#實(shí)現(xiàn)基本沒(méi)有影響。
4.從CIL代碼研究匿名函數(shù)捕獲變量和閉包的實(shí)質(zhì)
匿名函數(shù)不同于命名方法,可以訪問(wèn)它門(mén)外圍作用域的局部變量和環(huán)境。本文采用了一個(gè)例子說(shuō)明匿名函數(shù)(Lambda 表達(dá)式)可以捕獲外圍變量。而只要匿名函數(shù)有效,即使變量已經(jīng)離開(kāi)了作用域,這個(gè)變量的生命周期也會(huì)隨之?dāng)U展。這個(gè)現(xiàn)象被稱(chēng)為閉包。
?
4.1C#代碼
代碼如下:
(1)定義一個(gè)委托
delegate void DelTest(int n); View Code(2)在主函數(shù)中添加中添加代碼
int t = 10;DelTest delTest = (n) => {System.Console.WriteLine("{0}", t + n); };delTest(100); View Code輸出結(jié)果
110
4.2查看CIL代碼
分析類(lèi)結(jié)構(gòu)
分析Program::Main方法(主函數(shù))
.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() //創(chuàng)建一個(gè)對(duì)象IL_0005: stloc.1 //計(jì)算堆棧的頂部彈出當(dāng)前值并將其存儲(chǔ)到索引 1 處的局部變量列表中。 IL_0006: nopIL_0007: ldloc.1 //將索引 1 處的局部變量加載到計(jì)算堆棧上。IL_0008: ldc.i4.s 10 //將提供的 int8 值作為 int32 推送到計(jì)算堆棧上(短格式)。IL_000a: stfld int32 ClosureTest.Program/'<>c__DisplayClass1'::t //用新值替換在對(duì)象引用或指針的字段中存儲(chǔ)的值。IL_000f: ldloc.1 //將索引 1 處的局部變量加載到計(jì)算堆棧上。IL_0010: ldftn instance void ClosureTest.Program/'<>c__DisplayClass1'::'<Main>b__0'(int32) //將指向?qū)崿F(xiàn)特定方法的本機(jī)代碼的非托管指針(natural int 類(lèi)型)推送到計(jì)算堆棧上。IL_0016: newobj instance void ClosureTest.Program/DelTest::.ctor(object,native int) //創(chuàng)建一個(gè)對(duì)象IL_001b: stloc.0 //計(jì)算堆棧的頂部彈出當(dāng)前值并將其存儲(chǔ)到索引 0 處的局部變量列表中。IL_001c: ldloc.0 //將索引 0 處的局部變量加載到計(jì)算堆棧上。IL_001d: ldc.i4.s 100 //將提供的 int8 值作為 int32 推送到計(jì)算堆棧上(短格式)。IL_001f: callvirt instance void ClosureTest.Program/DelTest::Invoke(int32) //對(duì)對(duì)象調(diào)用后期綁定方法,并且將返回值推送到計(jì)算堆棧上。 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}" //推送對(duì)元數(shù)據(jù)中存儲(chǔ)的字符串的新對(duì)象引用。IL_0006: ldarg.0 //將索引為 0 的參數(shù)加載到計(jì)算堆棧上。IL_0007: ldfld int32 ClosureTest.Program/'<>c__DisplayClass1'::t //查找對(duì)象中其引用當(dāng)前位于計(jì)算堆棧的字段的值。IL_000c: ldarg.1 //將索引為 1 的參數(shù)加載到計(jì)算堆棧上。 IL_000d: add //將兩個(gè)值相加并將結(jié)果推送到計(jì)算堆棧上。 IL_000e: box [mscorlib]System.Int32 //將值類(lèi)轉(zhuǎn)換為對(duì)象引用(O 類(lèi)型)。IL_0013: call void [mscorlib]System.Console::WriteLine(string,object) //調(diào)用由傳遞的方法說(shuō)明符指示的方法。 IL_0018: nopIL_0019: ret } // end of method '<>c__DisplayClass1'::'<Main>b__0 CIL代碼
?
4.3分析
可以看到與之前的例子不同,CIL代碼中創(chuàng)建了一個(gè)叫做<>c__DisplayClass1的類(lèi),在類(lèi)中有一個(gè)字段public int32 t,和方法<Main>b__0,分別對(duì)應(yīng)要捕獲的變量和匿名函數(shù)的語(yǔ)句塊。
從主函數(shù)可以分析出流程
(1)創(chuàng)建一個(gè)<>c__DisplayClass1實(shí)例對(duì)象
(2)將<>c__DisplayClass1實(shí)例對(duì)象的字段t賦值為10
(3)創(chuàng)建一個(gè)DelTest委托類(lèi)的實(shí)例對(duì)象,將<>c__DisplayClass1實(shí)例對(duì)象的<Main>b__0方法傳遞給構(gòu)造函數(shù)
(4)調(diào)用DelTest委托,并將100作為參數(shù)
這時(shí)就不難理解閉包現(xiàn)象了,因?yàn)镃#其實(shí)用類(lèi)的字段來(lái)捕獲變量(無(wú)論值類(lèi)型還是引用類(lèi)型),所其作用域當(dāng)然會(huì)隨著匿名函數(shù)的生存周期而延長(zhǎng)。
4.4結(jié)論
C#在通過(guò)匿名函數(shù)實(shí)現(xiàn)需要捕獲變量的委托時(shí),需要做以下步驟
(1)創(chuàng)建一個(gè)類(lèi)(<>c__DisplayClass1)
(2)在類(lèi)中根據(jù)將要捕獲的變量創(chuàng)建對(duì)應(yīng)的字段(public int32 t)
(3)在類(lèi)中創(chuàng)建一個(gè)方法(<Main>b__0),用以實(shí)現(xiàn)匿名函數(shù)語(yǔ)句塊內(nèi)容
(4)創(chuàng)建類(lèi)(<>c__DisplayClass1)的對(duì)象,并用其方法(<Main>b__0)實(shí)例化委托
閉包現(xiàn)象則是因?yàn)椴襟E(2),捕獲變量的實(shí)現(xiàn)方式所帶來(lái)的附加產(chǎn)物。
需要捕獲變量的匿名函數(shù)在CIL代碼中實(shí)現(xiàn)原理圖
?
結(jié)論
C#在實(shí)現(xiàn)匿名函數(shù)(匿名方法和Lambda 表達(dá)式),是通過(guò)隱式的創(chuàng)建一個(gè)靜態(tài)方法或者類(lèi)(需要捕獲變量時(shí)),然后通過(guò)命名方式創(chuàng)建委托。
本文到這里筆者已經(jīng)完成了對(duì)匿名方法,Lambda 表達(dá)式和閉包的探索, 明白了這些都是C#為了方便用戶(hù)編寫(xiě)代碼而準(zhǔn)備的“語(yǔ)法糖”,其本質(zhì)并未超出.Net之前的范疇。
?
?
?
?
?
?
?
?
?
?
?
?
?
?
轉(zhuǎn)載于:https://www.cnblogs.com/max198727/p/3436220.html
總結(jié)
以上是生活随笔為你收集整理的C# 从CIL代码了解委托,匿名方法,Lambda 表达式和闭包本质的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: poj 3517
- 下一篇: c# dynamic 无法创建 泛型变量