首先來看一個簡單的例子。
var list = new Action[5];
for (int i = 0; i < list.Length; i++)
{
list[i] = () => { Console.WriteLine(i); };
}
foreach (var item in list)
{
item();
}
輸出結果爲:
5
5
5
5
5
通過這個簡單的例子,我來簡單講解一下C#中的閉包。
概念:
In essence, a closure is a block of code which can be executed at a later time, but which maintains the environment in which it was first created - i.e. it can still use the local variables etc of the method which created it, even after that method has finished executing.
大概的意思是:從本質上說,閉包是一段可以在晚些時候執行的代碼塊,但是這段代碼塊依然維護着它第一個被創建時環境(執行上下文)。 即它仍可以使用創建它的方法中局部變量,即使那個方法已經執行完了。
當然在 C# 中通常通過匿名函數和 Lamada 表達式來實現閉包。
經過搜尋,我在 msdn 的一篇博客中 見到了這樣一句話:
Because ()=>v means “return the current value of variable v“, not “return the value v was back when the delegate was created”. Closures close over variables, not over values
因爲()=> v意味着“返回變量v的當前值”,而不是“返回值v在委託創建時返回”。 閉合變量,而不是值” 。
也就是說,在委託中填入的變量,是最終的那個變量。這樣就合理解釋了上面爲何最終輸出的結果都爲5。因爲i
跳出循環時最終的值爲5
。
接着我們先看一下通過IL,(關於IL指令說明,可以參考這篇文章的最後http://blog.csdn.net/u010533180/article/details/53064257) 反編譯出來的代碼,建議大家根據上一篇文章畫流程圖。
.method private hidebysig static void ThreadThree() cil managed
{
// 代碼大小 130 (0x82)
.maxstack 4
.locals init ([0] class [mscorlib]System.Action[] list,
[1] class [mscorlib]System.Action 'CS$<>9__CachedAnonymousMethodDelegateb',
[2] class NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc' 'CS$<>8__localsd',
[3] class [mscorlib]System.Action item,
[4] bool CS$4$0000,
[5] class [mscorlib]System.Action[] CS$6$0001,
[6] int32 CS$7$0002)
IL_0000: nop
//將整數值 5 作爲 int32 推送到計算堆棧上。
IL_0001: ldc.i4.5
//將對新的從零開始的一維數組(其元素屬於特定類型)的對象引用推送到計算堆棧上。
IL_0002: newarr [mscorlib]System.Action
//從計算堆棧的頂部彈出當前值並將其存儲到索引 0 處的局部變量列表中。
IL_0007: stloc.0
// 將空引用(O 類型)推送到計算堆棧上。
IL_0008: ldnull
//從計算堆棧的頂部彈出當前值並將其存儲到索引 1 處的局部變量列表中。
IL_0009: stloc.1
//創建一個值類型的新對象或新實例,並將對象引用(O 類型)推送到計算堆棧上。
IL_000a: newobj instance void NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::.ctor()
//從計算堆棧的頂部彈出當前值並將其存儲到索引 2 處的局部變量列表中。
IL_000f: stloc.2
//將索引 2 處的局部變量加載到計算堆棧上。
IL_0010: ldloc.2
//將整數值 0 作爲 int32 推送到計算堆棧上。
IL_0011: ldc.i4.0
//用新值替換在對象引用或指針的字段中存儲的值。
IL_0012: stfld int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
//無條件地將控制轉移到目標指令(短格式)。等於轉移到了IL_0044指令
IL_0017: br.s IL_0044
IL_0019: nop
//將索引 0 處的局部變量加載到計算堆棧上。
IL_001a: ldloc.0
//將索引 2 處的局部變量加載到計算堆棧上。
IL_001b: ldloc.2
//查找對象中其引用當前位於計算堆棧的字段的值。等於查找i的值
IL_001c: ldfld int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
//將索引 1 處的局部變量加載到計算堆棧上。
IL_0021: ldloc.1
//如果 value 爲 true、非空或非零,則將控制轉移到目標指令(短格式)。 此時判斷指令IL_0021的值如果爲true ,則跳轉到指令IL_0033
IL_0022: brtrue.s IL_0033
//將索引 2 處的局部變量加載到計算堆棧上。
IL_0024: ldloc.2
//將指向實現特定方法的本機代碼的非託管指針(native int 類型)推送到計算堆棧上。
IL_0025: ldftn instance void NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::'<ThreadThree>b__a'()
//創建一個值類型的新對象或新實例,並將對象引用(O 類型)推送到計算堆棧上。
IL_002b: newobj instance void [mscorlib]System.Action::.ctor(object,
//從計算堆棧的頂部彈出當前值並將其存儲到索引 1 處的局部變量列表中。 native int)
IL_0030: stloc.1
//無條件地將控制轉移到目標指令(短格式)。等於轉移到了IL_0044指令
IL_0031: br.s IL_0033
//將索引 1 處的局部變量加載到計算堆棧上。
IL_0033: ldloc.1
//用計算堆棧上的對象 ref 值(O 類型)替換給定索引處的數組元素。這裏其實指的就是那個Action類型
IL_0034: stelem.ref
IL_0035: nop
//將索引 2 處的局部變量加載到計算堆棧上。
IL_0036: ldloc.2
//複製計算堆棧上當前最頂端的值,然後將副本推送到計算堆棧上。
IL_0037: dup
//查找對象中其引用當前位於計算堆棧的字段的值。等於查找i的值
IL_0038: ldfld int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
將整數值 1 作爲 int32 推送到計算堆棧上。
IL_003d: ldc.i4.1
//將兩個值相加並將結果推送到計算堆棧上。
IL_003e: add
//用新值替換在對象引用或指針的字段中存儲的值。
IL_003f: stfld int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
//將索引 2 處的局部變量加載到計算堆棧上。
IL_0044: ldloc.2
//查找對象中其引用當前位於計算堆棧的字段的值。等於查找i的值
IL_0045: ldfld int32 NowCoderProgrammingProject.ThreadDemo/'<>c__DisplayClassc'::i
//將索引 0 處的局部變量加載到計算堆棧上。
IL_004a: ldloc.0
// 將從零開始的、一維數組的元素的數目推送到計算堆棧上。
IL_004b: ldlen
// 將位於計算堆棧頂部的值轉換爲 int32。
IL_004c: conv.i4
// 比較兩個值。如果第一個值小於第二個值,則將整數值 1 (int32) 推送到計算堆棧上;反之,將 0 (int32) 推送到計算堆棧上。
IL_004d: clt
//從計算堆棧的頂部彈出當前值並將其存儲在局部變量列表中的 index 處(短格式)。
IL_004f: stloc.s CS$4$0000 即 CS$4$0000 這個所在的索引
//將特定索引處的局部變量加載到計算堆棧上(短格式)。
IL_0051: ldloc.s CS$4$0000
// 判斷此時是否ture,如果爲true 則跳轉到指令IL_0019
IL_0053: brtrue.s IL_0019
IL_0055: nop
//將索引 0 處的局部變量加載到計算堆棧上。
IL_0056: ldloc.0
//從計算堆棧的頂部彈出當前值並將其存儲在局部變量列表中的 index 處(短格式)。
IL_0057: stloc.s CS$6$0001
//將整數值 0 作爲 int32 推送到計算堆棧上。
IL_0059: ldc.i4.0
//從計算堆棧的頂部彈出當前值並將其存儲在局部變量列表中的 index 處(短格式)。
IL_005a: stloc.s CS$7$0002
// 無條件地將控制轉移到目標指令(短格式)。 轉移到IL_OO73
IL_005c: br.s IL_0073
//將特定索引處的局部變量加載到計算堆棧上(短格式)。
IL_005e: ldloc.s CS$6$0001
//將特定索引處的局部變量加載到計算堆棧上(短格式)。
IL_0060: ldloc.s CS$7$0002
//將位於指定數組索引處的包含對象引用的元素作爲 O 類型(對象引用)加載到計算堆棧的頂部。
IL_0062: ldelem.ref
//從計算堆棧的頂部彈出當前值並將其存儲到索引 3 處的局部變量列表中。
IL_0063: stloc.3
IL_0064: nop
// 將索引 3 處的局部變量加載到計算堆棧上。
IL_0065: ldloc.3
//調用虛方法 執行Action 方法
IL_0066: callvirt instance void [mscorlib]System.Action::Invoke()
IL_006b: nop
IL_006c: nop
//將特定索引處的局部變量加載到計算堆棧上(短格式)。
IL_006d: ldloc.s CS$7$0002
//將整數值 1作爲 int32 推送到計算堆棧上。
IL_006f: ldc.i4.1
//將兩個值相加並將結果推送到計算堆棧上。
IL_0070: add
//從計算堆棧的頂部彈出當前值並將其存儲在局部變量列表中的 index 處(短格式)。
IL_0071: stloc.s CS$7$0002
//將特定索引處的局部變量加載到計算堆棧上(短格式)。
IL_0073: ldloc.s CS$7$0002
//將特定索引處的局部變量加載到計算堆棧上(短格式)。
IL_0075: ldloc.s CS$6$0001
// 將從零開始的、一維數組的元素的數目推送到計算堆棧上。
IL_0077: ldlen
// 將位於計算堆棧頂部的值轉換爲 int32。
IL_0078: conv.i4
//比較兩個值。如果第一個值小於第二個值,則將整數值 1 (int32) 推送到計算堆棧上;反之,將 0 (int32) 推送到計算堆棧上。
IL_0079: clt
//將特定索引處的局部變量加載到計算堆棧上(短格式)。
IL_007b: stloc.s CS$4$0000
//將特定索引處的局部變量加載到計算堆棧上(短格式)。
IL_007d: ldloc.s CS$4$0000
//判斷此時的值是否爲true,如果爲true 則跳轉到指令IL_005e.這是應該判斷數組是否遍歷到了末尾
IL_007f: brtrue.s IL_005e
IL_0081: ret
} // end of method ThreadDemo::ThreadThree
.NET Reflector 反編譯的代碼:
Action[] actionArray = new Action[5];
Action action = null;
for (int i = 0; i < actionArray.Length; i++)
{
if (action == null)
{
action = () => Console.WriteLine(i);
}
actionArray[i] = action;
}
foreach (Action action2 in actionArray)
{
action2();
}
那麼上面的例子,如何輸出0-4呢?根據上句話的提示,只需要創建一個變量,保存當前運行狀態的值即可。修改後的結果爲:
var list = new Action[5];
for (int i = 0; i < list.Length; i++)
{
int localI = i;
list[i] = () => { Console.WriteLine(localI); };
}
foreach (var item in list)
{
item();
}
或者是添加一個額外的方法也行,這樣就相當於創建了一個局部變量。代碼如下:
var list = new Action[5];
for (int i = 0; i < list.Length; i++)
{
AddList(list, i);
}
foreach (var item in list)
{
item();
}
static void AddList(Action[] list, int i)
{
list[i] = () => { Console.WriteLine(i); };
}
上面兩種方法運行輸出的結果都爲:0-4.
通過上面的分析,加深理解了C#中的閉包,以後要謹慎使用。匿名函數和 Lambda 表達式給我們的編程帶來了許多快捷簡單的實現,如(List.Max((a)=>a.Level)等寫法)。但是我們要清醒的意識到這兩個糖果後面還是有個”坑“(閉包)。這再次告訴我們技術工作人,要”知其然,也要知其所以然“。
下面給出完整的代碼,其中有一些是我自己研究的,上面沒有給出分析,建議讀者自己分析,加深理解:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace NowCoderProgrammingProject
{
class ThreadDemo
{
public static void Main()
{
ThreadOne();
ThreadOne2();
ThreadTwo();
ThreadThree();
ThreadThree1();
}
private static void ThreadOne()
{
for (int i = 0; i < 10; i++)
{
Thread t = new Thread(() =>
{
Console.WriteLine(string.Format("{0}:{1}", Thread.CurrentThread.Name, i));
});
t.Name = string.Format("Thread{0}", i);
t.IsBackground = true;
t.Start();
}
Console.ReadLine();
}
private static void ThreadOne2()
{
for (int i = 0; i < 10; i++)
{
int localId = i;
Thread t = new Thread(() =>
{
Console.WriteLine(string.Format("{0}:{1}", Thread.CurrentThread.Name, localId));
});
t.Name = string.Format("Thread{0}", i);
t.IsBackground = true;
t.Start();
}
Console.ReadLine();
}
private static void ThreadTwo()
{
int id = 0;
for (int i = 0; i < 10; i++)
{
NewMethod(i, id++);
}
Console.ReadLine();
}
private static void NewMethod(int i, int readTimeID)
{
Thread t = new Thread(() =>
{
Console.WriteLine(string.Format("{0}:{1}", Thread.CurrentThread.Name, readTimeID));
});
t.Name = string.Format("Thread{0}", i);
t.IsBackground = true;
t.Start();
}
static void ThreadThree()
{
var list = new Action[5];
for (int i = 0; i < list.Length; i++)
{
list[i] = () => { Console.WriteLine(i); };
}
foreach (var item in list)
{
item();
}
}
static void ThreadThree1()
{
var list = new Action[5];
for (int i = 0; i < list.Length; i++)
{
int localI = i;
list[i] = () => { Console.WriteLine(localI); };
}
foreach (var item in list)
{
item();
}
}
static void ThreadThree2()
{
var list = new Action[5];
for (int i = 0; i < list.Length; i++)
{
AddList(list, i);
}
foreach (var item in list)
{
item();
}
}
static void AddList(Action[] list, int i)
{
list[i] = () => { Console.WriteLine(i); };
}
}
}
參考文章:
[1] Closing over the loop variable considered harmful
[2] Closing over the loop variable, part two