閉包定義
閉包(closure)在很多語言中都存在,在C#中,閉包是由匿名函數來表示的。C#中的閉包也叫做捕獲的變量。當一個匿名函數引用了他所在作用域(一般情況下是一個方法)的局部變量時,為了能夠順利的執行匿名函數而不至於包含它的函數執行完之后線程棧彈出導致局部變量消失,會將這個變量的生命周期延長。這時就形成了閉包。閉包利用了匿名函數的一個特性:因為編譯器會為匿名函數生成一個類(或結構),所以,提升匿名函數捕獲的這個變量的生命周期的方法就是在把這個變量放到這個類中。此外,這個類中定義的方法既是這個匿名函數。
示例
for循環中的閉包陷阱
我們在使用lambda的時候會遇到閉包,在閉包中有一個陷阱是在for循環中產生的,先上代碼:
class Program { static void Main(string[] args) { Action[] actions=new Action[5]; for (int i = 0; i < actions.Length; i++) { actions[i] = () => Console.WriteLine(i); } foreach (var item in actions) { item(); } Console.ReadKey(); } }
此時會看到Console輸出的是一連串的5,這是因為C#中在for塊中定義的int i會被當作外部變量來處理,我們在循環內部使用lambda的時候編譯器會給我們生成一個類,比如這個代碼如果是在Program中的main方法執行的時候這個類會在Program中生成,成為Program的一個內部類。這個類的主要作用是承載lambda表達式所代表的方法。當lambda表達式引用了一個局部變量時,為了保證這個變量的生命周期,這個局部變量會被編譯器生成的這個類所捕捉,也就是說,這個局部變量的生命周期得到了提升,成為了一個類級別的字段了。
模擬閉包
我想說的是如何避免這個for循環中閉包的陷阱呢?先來模擬一下編譯器在lambda背后的行為:
class Program { static void Main(string[] args) { Action[] actions = new Action[5]; var innerClass = new InnerClass();//關鍵在這里
int i;//for循環中定義的局部變量是被當作外部變量來使用的,這是在C#中的實現。 for (i = 0; i < actions.Length; i++) { innerClass.i = i; actions[i] = innerClass.DoIt; } foreach (var item in actions) { item(); } Console.ReadKey(); } public class InnerClass//這里是模擬編譯器為lambda表達式生成的類,我暫時命名為InnerClass,實際上編譯器生成的這個內部類有自己的命名規則。 { public int i;//這個是捕獲的for循環中的那個變量。 public void DoIt() { Console.WriteLine(i); } } }
閉包產生的這個陷阱關鍵就在於:
var innerClass = new InnerClass();//關鍵在這里
避免閉包陷阱
上面這句代碼的位置,之所以會產生陷阱,就是因為innerClass捕獲到的是最后的那個變量i的值,說到這里就不難想象如何去避免這個陷阱了,我們可以在for循環內部定義一個變量來保存每次的循環變量i的值:
class Program { static void Main(string[] args) { Action[] actions = new Action[5]; for (int i = 0; i < actions.Length; i++) { int j = i;//關鍵這里 actions[i] = Console.WriteLine(j); } foreach (var item in actions) { item(); } Console.ReadKey(); } }
我們在for循環的內部使用了一個變量先來捕獲一遍i,然后編譯器會將這個生成的類放在循環內部(而不是在for循環外部生成),每循環一次就生成一個新的來捕獲。牛逼吧編譯器?