前言
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() = ( 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'
主函數

.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
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() = ( 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'
主函數

.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
3.3分析
可以看出與上一節的例子唯一不同的是CIL代碼中生成的靜態函數需要傳遞一個string對象作為參數。
3.4結論
委托是否帶有參數對於C#實現基本沒有影響。
4.從CIL代碼研究匿名函數捕獲變量和閉包的實質
匿名函數不同於命名方法,可以訪問它門外圍作用域的局部變量和環境。本文采用了一個例子說明匿名函數(Lambda 表達式)可以捕獲外圍變量。而只要匿名函數有效,即使變量已經離開了作用域,這個變量的生命周期也會隨之擴展。這個現象被稱為閉包。
4.1C#代碼
代碼如下:
(1)定義一個委托

delegate void DelTest(int n);
(2)在主函數中添加中添加代碼

int t = 10; DelTest delTest = (n) => { System.Console.WriteLine("{0}", t + n); }; delTest(100);
輸出結果
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
分析<>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
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之前的范疇。