.NET:C# 如何实现的闭包?
背景
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、默认参数 等等,这些统称为“语法糖”。