前面一篇文章看到了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代碼看到了被捕獲變量的工作原理。