先不論什麼是閉包,什麼是閉包陷阱,我們開篇先看一段程式碼:
static void Main(string[] args)
{
List<Action> lists = new List<Action>();
for (int i = 0; i < 5; i++)
{
Action action = () => { Console.WriteLine(i); };
lists.Add(action);
}
foreach (var action in lists)
{
action();
}
Console.ReadLine();
}
那麼思考一下,控制檯輸出是什麼?
上述程式碼的本意是想讓 Action
宣告的匿名委託方法接收 i
的值,並輸出
0
1
2
3
4
但實際上,上述程式碼輸出的是
5
5
5
5
5
為什麼會這樣?就需要提到兩個個概念,變數作用域和閉包。
變數作用域就不說了,就是指變數可以被存取的範圍。例如全域性變數,區域性變數等。
閉包:簡單點說,就是函數和其參照的上下文的組合體,就是一個閉包。,例如上文程式碼中,for
迴圈內部,匿名方法內參照了變數 i
,那麼變數 i
和匿名方法 () => { Console.WriteLine(i); }
就組合成了一個閉包,在 for
迴圈中,變數 i
就是一個區域性變數,但是在閉包中,變數 i
對於匿名方法來說就是全域性變數。相當於這樣:
int i;
public void AnonymousMethod()
{
Console.WriteLine(i);
}
所以,當 for
迴圈結束時,在閉包內的全域性變數 i
的值就已經變成了5。則下面 foreach
程式碼每次執行輸出均為5。
實際上,編譯器在執行的時候,也確實為閉包生成了一個類,這個類只包含了一個方法和一個全域性變數。
來驗證一下,將上述程式碼編譯為dll後,通過ILDasm.exe工具檢視生成的IL程式碼
可以看到IL為閉包生成了一個類 <>c_DisplayClass0_0
,這個類只包含了一個變數 i
,如下:
還包含了一個匿名方法<Main>b_0:void
,從IL中,也可以看出,先取 <>c_DisplayClass0_0
的變數 i
,再呼叫控制檯輸出方法。如下:
接下來,在看一下整個控制檯程式 Main
方法的IL程式碼:
在IL_0007行,可以看到,宣告建立一個 Action
後就緊接著建立了一個 <>c_DisplayClass0_0
物件。且 for
迴圈的變數 i
就是 <>c_DisplayClass0_0
物件的變數 i
(注意這裡指的是參照,並不是值),這樣,在迴圈結束的時候,物件的變數 i
就變成了5。
這樣就可以解釋為什麼輸出會是5了。
:::info{title="相關資訊"}
需要注意的是,Action action = () => { Console.WriteLine(i); };
這句程式碼只是宣告了一個委託,委託繫結的是一個匿名方法,並沒有真正執行,只有呼叫該委託的時候才真正執行。比如 action()
或 action.Invoke()
。
:::
在上面的探究原理的過程中,其實也發現了追根究底的問題其實就是,在建立閉包物件的時候,參照的區域性變數,在外部被修改(比如上面程式碼中的for
迴圈的變數 i
就是閉包物件的變數 i
,指的是指標是同一個),那麼可以在建立閉包物件的時候,重新建立一個指標物件,將預期值賦值給它就可以了,比如上面的範例程式碼可以這樣修改:
static void Main(string[] args)
{
List<Action> lists = new List<Action>();
for (int i = 0; i < 5; i++)
{
int temp = i;
Action action = () => { Console.WriteLine(temp); };
lists.Add(action);
}
foreach (var action in lists)
{
action();
}
Console.ReadLine();
}
這樣,閉包物件的變數就不再是 for
迴圈的 i
了,也就不會再被修改。輸出結果為:
0
1
2
3
4
作者: Niuery Daily
出處: https://www.cnblogs.com/pandefu/>
關於作者:.Net Framework,.Net Core ,WindowsForm,WPF ,控制元件庫,多執行緒
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出 原文連結,否則保留追究法律責任的權利。 如有問題, 可郵件諮詢。