温故知新---重读C#InDepth(二)

一本好书,或是一本比较有深度的书,就是每次研读的时候都会有新的发现。

好吧,我承认每次读的时候都有泛泛而过的嫌疑~~

这几年一直专注于C#客户端的开发,逐步从迷迷糊糊,到一知半解,再到自以为是,最后沉下心重新审视。也许这也是一种进步一种自我学习的过程。

前面啰嗦了这么多,希望大家也能不那么浮躁的“深入理解”C#这门语言的每个知识点。本文总结书本中的知识,在结合实际应用场合进行概述,如果有不正确的地方,还请不吝指教。

文章中的内容比较浅显,请高手略过此文。

 

4. 程序闭包

程序闭包的问题是由于程序对某些变量进行了预判和处理(个人理解,若有误或不足请指正)使得某些变量理应作为值类型却变为了引用类型导致数据异常。

当然,大多数情况下,我们是不会遇到这样的问题,但在某些情况下,我们不得不注意并分析问题的根本原因,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)方法中,可是偏偏却逃离了方法的作用域,这就是我们所说的值类型是在堆上Or栈上
完全取决于其初始化的位置是在栈上还是在堆上

 

例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

这个例子摘自编写高质量代码:改善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变为引用类型,数据异常。

 

 

持续更新:示例代码下载

posted @ 2014-07-25 09:50  史蒂芬King  阅读(1179)  评论(0编辑  收藏  举报