背景
C# 在編譯器層面為我們提供了閉包機制(Java7 和 Go 也是這種思路),本文簡單的做個解釋。
背景知識
你必須了解:引用類型、值類型、引用、對象、值類型的值(簡稱值)。
關於引用、對象和值在內存的分配有如下幾點規則:
- 對象分配在堆中。
- 作為字段的引用分配在堆中(內嵌在對象中)。
- 作為局部變量(參數也是局部變量)的引用分配在棧中。
- 作為字段的值分配在堆中(內嵌在對象中)。
- 作為局部變量(參數也是局部變量)的值用分配在棧中。
- 局部變量只能存活於所在的作用域(方法中的大括號確定了作用域的長短)。
注:按值傳遞和按引用傳遞也是需要掌握的知識點,C# 默認是按值傳遞的。
閉包示例
測試代碼
1 private static void Before() 2 { 3 Action[] actions = new Action[10]; 4 5 for (var i = 0; i < actions.Length; i++) 6 { 7 actions[i] = () => 8 { 9 Console.WriteLine(i); 10 }; 11 } 12 13 foreach (var item in actions) 14 { 15 item(); 16 } 17 }
輸出結果
編譯器幫我們做了是什么?
編譯器幫我們生成的代碼(我自己寫的,可以使用 Reflector 工具自己查看)
1 private static void After() 2 { 3 Action[] actions = new Action[10]; 4 5 var anonymous = new AnonymousClass(); 6 7 for (anonymous.i = 0; anonymous.i < actions.Length; anonymous.i++) 8 { 9 actions[anonymous.i ] = anonymous.Action; 10 } 11 12 foreach (var item in actions) 13 { 14 item(); 15 } 16 } 17 18 class AnonymousClass 19 { 20 public int i; 21 22 public void Action() 23 { 24 Console.WriteLine(this.i); 25 } 26 }
如何修復上面的問題?
上面的例子不是我們期望的輸出,讓我們給出兩種修改方案:
第一種(借鑒JS)
1 private static void Fix() 2 { 3 Action[] actions = new Action[10]; 4 5 for (var i = 0; i < actions.Length; i++) 6 { 7 new Action<int>((j) => 8 { 9 actions[i] = () => 10 { 11 Console.WriteLine(j); 12 }; 13 })(i); 14 } 15 16 foreach (var item in actions) 17 { 18 item(); 19 } 20 }
第二種
1 public static void Fix2() 2 { 3 Action[] actions = new Action[10]; 4 5 for (var i = 0; i < actions.Length; i++) 6 { 7 var j = i; 8 9 actions[i] = () => 10 { 11 Console.WriteLine(j); 12 }; 13 } 14 15 foreach (var item in actions) 16 { 17 item(); 18 } 19 }
分析
編譯器將閉包引用的局部變量轉換為匿名類型的字段,導致了局部變量分配在堆中。
備注
C# 編譯器幫我們做了非常多的工作,如:自動屬性、類型推斷、匿名類型、匿名委托、Lamda 表達式、析構方法、await 和 sync、using、對象初始化表達式、lock、默認參數 等等,這些統稱為“語法糖”。