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以前的範疇。

相關文章
相關標籤/搜索