• 对Closure的再思考


    前些日子我对Closure做了初步的思考, 却总觉得意犹未尽,感觉Closure还没有进入到我心里去,还没有真正地、完全地理解。好吧,就把近日来杂乱的思考整理一下写出来吧。

    为了便于理解,就让我从一些废话开始吧。众所周知,在C#中有两种类型——值类型和引用类型。就从引用类型谈起吧 。

    引用类型的回顾

    对于下面的代码:

    object obj = new object();
    object A = obj;
    object B = obj;
    
    有如下的图示关系。图中矩形表示内存中的object对象,在上面的代码中只有一个object类型的对象被实例化,而A,B以及obj只是这个对象的三个"名字"而已。 

    在原有代码的基础上做如下的修改:

    object C = new object();
    A = C;
    

    则内存中的对象和对象的"名字"之间则有如下的关系:

    如果您能读到这儿我已经理解您此刻的心情了,毕竟这实在没什么好谈论的。但是,十分抱歉的要告诉您,在这个小问题上我还要再谈最后一点,假如全部的代码是如下的这个样子:

    object obj = new object();
    object A = obj;
    object B = A; 
    

    内存中对象及其名字仍然同第一个例子一样:

    需要强调的一点是,上面的代码不可能达到如下图示的效果。毕竟"名字"并不是一个实体,任何一个"名字"都要依附于一个实体,下图中"名字"B依附于"名字"A是不正确的。

    代理类型的理解

    上面对引用类型做了一个简单的回顾,那么当我们研究的对象是一个代理类型时,结果又是什么样子呢?

    对于如下的代码的输出结果应该没什么问题,其对应的图示也紧跟其后。

    public static void Main()
    {
        Action action1 = MethodA;
        Action action2 = MethodA;
        action1();
        action2();
    }
    
    public static void MethodA()
    {
        Console.WriteLine("Method A");
    }
    

    对于上面引用类型所得出的一些结论也完全适用于这里代理的情况。其实完全可以这样理解:方法可以理解为一个实实在在的对象,而代理便可理解为这个方法的一个"名字"或是"别名"(此处指可以这样理解,并不是说是完全是这种情况,多播代理可是一次可以代表多个函数的)。如果您对代理之间的关系持怀疑态度,可仿照上面引用的例子,做简单的测试即可,本文此处不做赘述。

    Closure(闭合)的陷阱

    有幸拜读过Andrew Koening的大作<<C Traps and Pitfalls>>,其让我彻底折服,在此我也斗胆借用一下Traps(陷阱)这个词来谈论一下Closure。或许谈不上是Closure本身的陷阱吧,毕竟这个陷阱是由C#对Closure特殊的实现而造成的,这平白无故的让Closure背了一个黑锅。

    问题的引出

    通常用lambda表达式表示的递归一般是这个样子(此处以阶乘函数的实现来举例):

    Func<int, int> f = null;
    f = n => n > 0 ? f(n - 1) * n : 1; //f represents factorial function
    

    (注意:Func<int, int> f = n => n > 0 ? f(n - 1) * n : 1;不能通过编译的。

    补充:有些观点说这不是一个递归,这个留待以后再做讨论,在此姑且理解之为递归吧。)

    上面的代码运行没有任何问题。假如,把代码改成如下的样子:

    Func<int, int> f = null;
    f = n => n > 0 ? f(n - 1) * n : 1; //f represents factorial function
    Func<int, int> factorial = f;
    

    则执行代理f和代理factorial应该产生一样的结果,事实上也是这样。在此代码基础上,再做如下的修改:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace Learning
    {
        public class LearningClosure
        {
            public static void Main()
            {
                Func<int, int> f = null;
                f = n => n > 0 ? f(n - 1) * n : 1; //f represents factorial function
                Func<int, int> factorial = f;
                f = n => n * 2;
                Console.WriteLine(f(3));
                Console.WriteLine(factorial(3));
            }
        }
    }
    

    其运行结果是6, 12。 然而按照程序编写的意图来想,期望的输出结果应该是6,6

    问题的分析

    本文的开始我对引用关系做的长篇幅介绍就是为了解释这个问题而做的铺垫的,目的是为了使这个理解过程变的更容易。Lambda表达式给我们带来方便的同时也掩盖了很多实现的细节,因此要解释上面的问题还要从lambda内部实现的细节开始。

    前面讲述了一个代理其实就是一个具体函数的"名字"或者称之为"别名",归根结底,它要指向一个具体函数的,这个函数就是编译器根据lambda自动生成的。要看这些编译器生成的内容是需要一定工具的,比如ILAsm或Reflector等。对于这个问题就不用看IL代码了,毕竟那是比较令人头疼的事情。只要用Reflector把代码翻译成C# 1.0版本就可以查看一切逻辑了。编译器生成的代码由于命名的特殊性,阅读起来也是有一定难度的,此处我就把经过我重新命名的代码列在下面(关键部分代码):

    这一段代码

    Func<int, int> f = null;
    f = n => n > 0 ? f(n - 1) * n : 1; 
    

    被编译器实际编译成了如下的代码(为了方便阅读,对编译器生成的名字做了修改)

    private sealed class FactorialClosure
    {
        // Fields
        public Func<int, int> f;
    
        // Methods
        public int FactorialMethod(int n)
        {
            if (n <= 0)
            {
                return 1;
            }
            return (this.f(n - 1) * n);
        }
    }
    

    可以看出,编译器生成了一个类(此处叫FactorialColsure),类里面定义了一个代理f,这个f就是我们代码中写的那个f;又定义了一个函数(FactorialMethod),这个函数看起来就是我们代码中的lambda表达式的声明部分。在编译器生成的代码中f并没有被赋值,那么它是在哪儿被赋值的呢?答案是在主函数的调用中:

    public static void Main()
    {
        FactorialClosure facClosure = new FactorialClosure();
        facClosure.f = null;
        facClosure.f = new Func<int, int>(facClosure.FactorialMethod);
        //...
    }
    

    由此可见,f被赋值为FactorialMethod这个函数,而FactorialMethod这个函数又调用了f,由此便形成了一个递归调用。再次,我需要再次罗嗦强调一遍,实例代码中的Main函数中f就是完完全全的FactorialClosure.f,而Main函数中却不存在一个局部变量f。另一个方面,Main函数中Func<int, int> factorial = f;语句却定义了一个Main函数内的局部变量factorial,这个factorial指向的是FactorialMethod这个方法(记住FactorialMethod内部调用的是FactorialClosure.f)。在结合上面的各个要点理解之后,实例代码的输出结果就容易理解了。下面给出全部的编译器生成代码:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace Learning
    {
        public class LearningClosure
        {
            private static Func<int, int> doubleClosureInstance = null;
    
            private static int DoubleClosure(int n)
            {
                return n * 2;
            }
    
            private sealed class FactorialClosure
            {
                public Func<int, int> f;
    
                public int FactorialMethod(int n)
                {
                    if (n <= 0)
                    {
                        return 1;
                    }
                    return (this.f(n - 1) * n);
                }
            }
    
            public static void Main()
            {
                FactorialClosure facClosure = new FactorialClosure();
                facClosure.f = null;
                facClosure.f = new Func<int, int>(facClosure.FactorialMethod);
                Func<int, int> factorial = facClosure.f;
                if (doubleClosureInstance == null)
                {
                    doubleClosureInstance = DoubleClosure;
                }
                facClosure.f = doubleClosureInstance;
                Console.WriteLine(facClosure.f(3));
                Console.WriteLine(factorial(3));    
            }
        }
    }
    

    结论

    对匿名代理、lambda表达式以及它们产生的Closure一定要保持清醒的思路,了解了其真正的机理才能在实际开发中减少bug的同时最大化发挥Closure的威力。

  • 相关阅读:
    使用Visual Studio .Net 做自己的汉化软件
    给所有的Control加两个属性,实现回车键自动跳转到下一个控件
    数字逗号标记—以前原创(一)
    解决w3wp.exe占用CPU和内存问题
    sql日期函数
    索引的使用总结
    w3wp.exe狂占内存
    w3wp.exe占内存CPU问题 WIN2003 IIS6.0假死现象的分析
    查看Linux系统日志
    linux动态增加LV空间
  • 原文地址:https://www.cnblogs.com/lsp/p/1655444.html
Copyright © 2020-2023  润新知