一本好书,或是一本比较有深度的书,就是每次研读的时候都会有新的发现。
好吧,我承认每次读的时候都有泛泛而过的嫌疑~~
这几年一直专注于C#客户端的开发,逐步从迷迷糊糊,到一知半解,再到自以为是,最后沉下心重新审视。也许这也是一种进步一种自我学习的过程。
前面啰嗦了这么多,希望大家也能不那么浮躁的“深入理解”C#这门语言的每个知识点。本文总结书本中的知识,在结合实际应用场合进行概述,如果有不正确的地方,还请不吝指教。
文章中的内容比较浅显,请高手略过此文。
程序闭包的问题是由于程序对某些变量进行了预判和处理(个人理解,若有误或不足请指正)使得某些变量理应作为值类型却变为了引用类型导致数据异常。
当然,大多数情况下,我们是不会遇到这样的问题,但在某些情况下,我们不得不注意并分析问题的根本原因,BUG永远不是随机的。
下面通过几个例子逐步来理解闭包的概念:
例4.1
private void Button1_Click(object sender, RoutedEventArgs e) { int outerVariableCaptured = 5; // 外部变量(被捕获) int outerVariableUnCaptured = 50; // 外部变量(未捕获) if (DateTime.Now.Hour <= 24) { int normalLocalVariable = 1; // 普通方法的局部变量,不是外部变量,因为在其作用域内无匿名方法。 this.Txb_Msg.Text += string.Format("普通方法的局部变量 = {0}", normalLocalVariable) + System.Environment.NewLine; } Action x = new Action(() => { int anonLocal = 2; // 匿名方法的局部变量 this.Txb_Msg.Text += string.Format("匿名方法的局部变量 = {0}", anonLocal) + System.Environment.NewLine; this.Txb_Msg.Text += string.Format("匿名方法中被捕获的外部变量 = {0}", outerVariableCaptured) + System.Environment.NewLine; // 匿名方法中调用了作用域外的变量,所以变量变为被捕获的外部变量 }); this.Txb_Msg.Text += string.Format("普通方法的未捕获的外部变量 = {0}", outerVariableUnCaptured) + System.Environment.NewLine; x(); }
输出结果: 普通方法的局部变量 = 1 普通方法的未捕获的外部变量 = 50 匿名方法的局部变量 = 2 匿名方法中被捕获的外部变量 = 5
4.1中是让大家了解外部变量,局部变量,捕获等相关概念。其中最重要的是被捕获的外部变量。
例4.2
private void Button3_Click(object sender, RoutedEventArgs e) { // 证明被捕捉的局部变量声明周期被延长了。 OnCreateDelegate += MainWindow_OnCreateDelegate; this.Dispatcher.Invoke(OnCreateDelegate(this)); // 此处Invoke容易引起歧义:原因在于Invoke事件之后返回的还是一个事件。 // Counter是值类型,逃脱其作用域时栈上数据会被回收,真实的情况是这样吗? // 从另一个侧面也说明了,值类型是在栈上还是堆上,依赖于创建对象的类型。 }
private Delegate MainWindow_OnCreateDelegate(object sender) { var frm = sender as MainWindow; int Counter = 1; var a = new Action(() => { frm.Txb_Msg.Text += string.Format("委托内部的变量值 = {0}", Counter) + System.Environment.NewLine; Counter++; }); a(); return a; }
输出: 委托内部的变量值 = 1 委托内部的变量值 = 2
这个例子要说明的是Counter其值类型原本的生存周期应该在MainWindow_OnCreateDelegate(object sender)方法中,可是偏偏却逃离了方法的作用域,这就是我们所说的值类型是在堆上海事栈上
完全取决于其初始化的位置是在栈上还是在堆上。
例4.3
private void Button4_Click(object sender, RoutedEventArgs e) { // 更复杂的一些情况 var methods = new Action[2]; int outside = 10; // 实例化变量一次 for (int i = 0; i < 2; i++) { int inside = 100; // 实例化变量多次 methods[i] = new Action(() => { this.Txb_Msg.Text += string.Format("Inside Value = {0}; Outside Value = {1} ", inside, outside) + System.Environment.NewLine; inside++; outside++; // 匿名方法捕获的变量 }); } methods[0].Invoke(); methods[0].Invoke(); methods[0].Invoke(); methods[1].Invoke(); }
输出结果: /***************Outside变量内存共享************* * Inside Value = 100; Outside Value = 10 * Inside Value = 101; Outside Value = 11 * Inside Value = 102; Outside Value = 12 * Inside Value = 100; Outside Value = 13 * *******************************************/
这个例子就得好好想想了,outside和inside的值到底会是什么?为什么会这样?
原因在于inside在For循环的内部初始化了多次,也就是说For循环几次,就有几个独立的inside对象,虽说它是值类型。
例4.4
这个例子摘自《class="lemmaTitleH1">编写高质量代码:改善C#程序的157个建议》
private void Button5_Click(object sender, RoutedEventArgs e) { // 闭包陷阱 var methods = new Action[2]; for (int i = 0; i < 2; i++) { int inside = i; // 实例化变量多次 methods[i] = new Action(() => { this.Txb_Msg.Text += string.Format("Inside Value = {0}; Index Value = {1} ", inside, i) + System.Environment.NewLine; }); } methods[0].Invoke(); methods[1].Invoke(); /*****************闭包陷阱********************* * 当使用i的值时,i就是前面说的共享变量(捕获的外部变量),所以总是输出i的最大值。 * 当使用Inside值时,Inside就是内部变量,每次创建对象都重新生成,所以此处inside的值是递增,即缓存下i的值。 * 对于IL,其创建了Tempclass.i来代替i,导致了i值共享。 * *******************************************/ }
输出结果: Inside Value = 0; Index Value = 2 Inside Value = 1; Index Value = 2
其实如果用ILDasm来看的话,针对i这个对象,IL生成了一个DisplayClass(就是一个名字而已)这样一个类,最总导致了,i变为引用类型,数据异常。
持续更新:示例代码下载