前面一篇文章看到了C# 2.0中通過匿名方法來簡化委托(見 http://www.linuxidc.com/Linux/2015-02/114153.htm),下面來看看匿名方法中的變量。
閉包的基本概念是:一個函數除了能夠通過提供給它的參數與環境交互之外,還能同環境進行更大程度的互動。對於C# 2.0中出現的匿名方法的閉包表現為,匿名方法能使用在聲明該匿名方法的方法內部定義的局部變量。
在進一步了解閉包之前,我們先看看下面兩個術語:
外部變量(outer variable):是指其作用域(scope)包括一個匿名方法的局部變量或參數(ref和out參數除外)
被捕捉的外部變量(captured outer variable):它是在匿名方法內部使用的外部變量
結合上面的解釋,來看一個被捕獲的變量的例子:
private static void EnclosingMethod() { //未被捕獲的外部變量 int outerVariable = 2; //被匿名方法捕獲的外部變量 string capturedVariable = "captured variable"; if (DateTime.Now.Hour == 23) { //普通局部變量 int normalLocalVarialbe = 3; Console.WriteLine(normalLocalVarialbe); } Action x = delegate { //匿名方法的局部變量 string anonymousLocal = "local variable of anonymous method"; //獲得被捕獲的外部變量 Console.WriteLine(capturedVariable); Console.WriteLine(anonymousLocal); }; x(); }
一個變量被捕獲之後,被匿名方法捕獲的是這個變量,為不是創建委托實例時該變量的值。下面通過一個例子來看看這句描述。
private static void CapturedVariableTesting() { string captured = "before x is created"; Action x = delegate { Console.WriteLine(captured); captured = "changed by x"; }; captured = "changed before x is invoked"; x(); Console.WriteLine(captured); captured = "before second invocation"; x(); }
代碼的輸出為:
在CapturedVariableTesting這個方法中,我們始終都是在使用同一個被捕獲變量captured;也就是說,在匿名方法外對被捕獲變量的修改,在匿名方法內部是可見的,反之亦然。
閉包的出現給我們帶來很多的便利,直接利用被捕獲變量可以簡化編程,避免專門創建一些類來存儲一個委托需要處理的信息。
看一個例子,我們給定一個上限,來獲取List中所有小於這個上限的數字。
private static List<int> FindAllLessThan(List<int> numList, int upperLimitation) { return numList.FindAll(delegate(int num) { return num < upperLimitation; }); }
由於閉包的出現,我們不用將upperLimitation這個變量以函數參數的形式傳給匿名函數,在匿名方法中可以直接使用這個被捕獲的變量。
前面看到的例子都比較簡單,下面我們看一個稍微復雜的例子:
static void Main(string[] args) { Action x = CreateDelegateInstance(); x(); x(); Console.Read(); } private static Action CreateDelegateInstance() { int counter = 5; Action ret = delegate { Console.WriteLine(counter); counter++; }; ret(); return ret; }
代碼輸出為:
為什麼結果是5,6,7?變量counter在CreateDelegateInstance方法結束後為什麼沒有被銷毀?
當我們查看這個例子的IL代碼時,發現編譯器為我們創建了一個類"<>c__DisplayClass1"。
.class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass1' extends [mscorlib]System.Object { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Fields .field public int32 counter // Methods .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x2078 // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: ret } // end of method '<>c__DisplayClass1'::.ctor .method public hidebysig instance void '<CreateDelegateInstance>b__0' () cil managed { // Method begins at RVA 0x2080 // Code size 28 (0x1c) .maxstack 8 IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld int32 AnonymousMethod.Program/'<>c__DisplayClass1'::counter IL_0007: call void [mscorlib]System.Console::WriteLine(int32) IL_000c: nop IL_000d: ldarg.0 IL_000e: dup IL_000f: ldfld int32 AnonymousMethod.Program/'<>c__DisplayClass1'::counter IL_0014: ldc.i4.1 IL_0015: add IL_0016: stfld int32 AnonymousMethod.Program/'<>c__DisplayClass1'::counter IL_001b: ret } // end of method '<>c__DisplayClass1'::'<CreateDelegateInstance>b__0' } // end of class <>c__DisplayClass1
而在CreateDelegateInstance方法的IL代碼中可以看到,CreateDelegateInstance的局部變量counter實際上就是"<>c__DisplayClass1"對象的counter字段。
IL_0000: newobj instance void AnonymousMethod.Program/'<>c__DisplayClass1'::.ctor() IL_0005: stloc.1 IL_0006: nop IL_0007: ldloc.1 IL_0008: ldc.i4.5 IL_0009: stfld int32 AnonymousMethod.Program/'<>c__DisplayClass1'::counter
通過上面的分析可以看到,編譯器創建了一個額外的類來容納變量,CreateDelegateInstance方法擁有該類的一個實例引用,並通過這個引用訪問counter變量。counter這個局部變量並不是在"調用棧"空間上,這也就解釋了為什麼函數返回後,這個變量沒有被銷毀。
在上面的例子中只有一個委托實例,下面再看一個擁有多個委托實例的例子:
static void Main(string[] args) { List<Action> list = new List<Action>(); for(int index = 0; index < 5; index++) { int counter = index * 10; list.Add(delegate { Console.WriteLine(counter); counter++; }); } foreach (Action x in list) { x(); } list[0](); list[0](); list[1](); Console.Read(); }
代碼輸出為:
通過輸出可以看到,每個委托實例將捕獲不同的變量。
所以被捕獲變量的聲明期可以總結為:對於一個被捕獲的變量,只要還有任何委托實例在引用它,它就會一直存在;當一個變量被捕獲時,捕獲的是變量的"實例"。
本文介紹了閉包和不同的變量類型。在匿名方法中,通過被捕獲變量,我們可以使用"現有"的上下文信息,而不必專門設置額外的類型來存儲一些已知的數據。
同時,介紹了被捕獲變量的生命期,通過IL代碼看到了被捕獲變量的工作原理。