《代码的未来》读书笔记:也谈闭包

一、从函数对象到委托

松本大叔说:要理解闭包,从函数指针开始!

1.1 函数指针及其作用

  原文中使用了C语言的函数对象,这里我们主要从.NET平台来说。在.NET中,委托这个概念对C++程序员来说并不陌生,因为它和C++中的函数指针非常类似,很多码农也喜欢称委托为安全的函数指针。无论这一说法是否正确,委托的的确确实现了和函数指针类似的功能,那就是提供了程序回调指定方法的机制

  下面的代码展示了委托的基本使用:

    // 定义的一个委托
    public delegate void TestDelegate(int i);

    public class Program
    {
        public static void Main(string[] args)
        {
            // 定义委托实例
            TestDelegate td = new TestDelegate(PrintMessage);
            // 调用委托方法
            td(0);
            td(1);

            Console.ReadKey();
        }

        public static void PrintMessage(int i)
        {
            Console.WriteLine("这是第{0}个方法!", i.ToString());
        }
    }

  运行结果如下图所示:

  

  也许很多初学者都了解委托的概念,但是却不知道为什么要有委托,委托到底有什么作用?这里我们通过数据结构里面最经典的冒泡排序来看看委托在提高程序扩展性/通用性方面的作用。

  Step1.我们可以比较容易地写出下面的这段冒泡排序代码,它针对int类型数组进行升序排序:

    public static void BubbleSort(int[] arr)
    {
        int i, j;
        int temp;
        bool isExchanged = true;

        for (j = 1; j < arr.Length && isExchanged; j++)
        {
            isExchanged = false;
            for (i = 0; i < arr.Length - j; i++)
            {
                if (arr[i] > arr[i + 1])
                {
                    // 核心操作:交换两个元素
                    temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                    // 附加操作:改变标志
                    isExchanged = true;
                }
            }
        }
    }

  Step2.但是我们不能只能只对int类型进行排序吧,难道对double类型还得重写?于是我们加入.NET中的模板—泛型

    public static void BubbleSort(T[] arr)
    {
        int i, j;
        T temp;
        bool isExchanged = true;

        for (j = 1; j < arr.Length && isExchanged; j++)
        {
            isExchanged = false;
            for (i = 0; i < arr.Length - j; i++)
            {
                if (arr[i].CompareTo(arr[i + 1]) > 0)
                {
                    // 核心操作:交换两个元素
                    temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                    // 附加操作:改变标志
                    isExchanged = true;
                }
            }
        }
    }

  Step3.但是我们不能只进行升序排序吧,如果要降序排序是不是要还得重写排序算法,于是我们加入.NET中的函数指针—委托

    public static void BubbleSort(T[] arr, Comparison<T> comp)
    {
        int i, j;
        T temp;
        bool isExchanged = true;

        for (j = 1; j < arr.Length && isExchanged; j++)
        {
            isExchanged = false;
            for (i = 0; i < arr.Length - j; i++)
            {
                if (comp(arr[i], arr[i + 1]) > 0)
                {
                    // 核心操作:交换两个元素
                    temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                    // 附加操作:改变标志
                    isExchanged = true;
                }
            }
        }
    }

  其中,Comparison委托是.NET中的一个预定义委托,主要用来进行元素的比较。对Comparison不熟悉的朋友,可以看看《.NET中那些所谓的新语法之预定义委托》。

  这时,我们如果想要进行升序排序,只需通过以下方式调用:

    int[] smallDatas = { 3, 6, 5, 9, 7, 1, 8, 2, 4 };
    SortingHelper<int>.BubbleSort(smallDatas, new Comparison<int>((p1, p2) => p1 - p2));

  运行结果如下:

  

  过了一段时间,我们想要进行降序排序,只需改变委托的实现即可复用代码:

    int[] smallDatas = { 3, 6, 5, 9, 7, 1, 8, 2, 4 };
    SortingHelper<int>.BubbleSort(smallDatas, new Comparison<int>((p1, p2) => p2 - p1));

  运行结果如下:

  

  两次重构之后,我们的这个冒泡排序代码的通用性就提高了不少,可以看到委托在其中起到了很大的作用。

1.2 函数指针的局限

  这里松本大叔举了一个例子,我这里使用C#语言来描述。对一个由各个节点构成的链表进行两种不同方式的遍历,一是通过一般的循环,二是通过函数指针(这里主要是指委托),本质的部分是从main方法开始的。

  (1)节点定义

    /// <summary>
    /// 链表节点定义
    /// </summary>
    public class Node
    {
        public Node next;
        public int value;
    }

  (2)委托定义

    // 委托类型定义-函数指针类型
    public delegate void funct(int num);

  (3)使用委托的自定义遍历方法与符合委托定义的方法

    static void Foreach(Node list, funct func)
    {
        while (list != null)
        {
            func(list.value);
            list = list.next;
        }
    }

    static void F(int num)
    {
        Console.WriteLine("Node[?]={0}", num);
    }

  (4)主入口:main方法

    const int size = 4;

    static void Main(string[] args)
    {
        int i = 1;
        Node head = new Node();
        head.value = 0;
        head.next = null;
        // 创建链表
        while (i < size)
        {
            Node node = new Node();
            node.value = i;
            node.next = head;
            head = node;

            i++;
        }

        i = 0;
        Node list = head;
        // 遍历链表
        while (list != null)
        {
            Console.WriteLine("Node[{0}]={1}", i++, list.value);
            list = list.next;
        }
        // 自定义遍历
        Foreach(head, F);

        Console.ReadKey();
    }

  该程序的运行结果如下:

  

  其中前面4行是while循环的输出结果,而后4行则是自定义Foreach循环的输出结果。可以明显看出,在while循环的输出结果中,可以显示出索引,而Foreach的结果中只能显示"?"。这是因为:与while语句不通,Foreach的循环实际上是在另一函数中执行的,因此无法从函数中访问位于外部的局部变量 i。当然,如果 i 是一个全局变量就不存在这个问题了,不过为了这个目的而使用副作用很大的全局变量也并不是一个好主意。因此,“对外部(局部)变量的访问”是函数指针(这里主要指委托)的最大弱点

  我们已经知道了函数指针的缺点,那么为了克服这个缺点,就可以开始认知这次的主题—闭包

二、JavaScript闭包初探

谈到闭包,得使用一种支持闭包的语言,而这方面,JavaScript绝对是棒棒哒!但是松本大叔说:要理解闭包,得先了解两个术语:作用域和生存周期。

2.1 作用域(Scope)

  作用域指的是变量的有效范围,也就是某个变量可以被访问的范围。在JavaScript中,保留字var所表示的变量所表示的变量声明所在的最内侧代码块就是作用域的单位,而没有进行显示声明的变量就是全局变量。

  作用域是嵌套的,因此位于内侧的代码块可以访问以其自身为作用域的变量,以及以外侧代码块为作用域的变量。

  下图中我们将匿名函数赋值给了一个变量(当然,如果不赋值而直接作为参数传递也是可以的),这个函数对象也有自己的作用域:

  我靠,JavaScript中可以直接定义函数对象,那么,上面程序中的Foreach方法用JavaScript就可以更直接地写出来。

    function foreach(list, func) {
        while (list) {
            func(list.val);
            list = list.next;
        }
    }

    var list = null;    // 变量声明
    for (var i = 0; i < 4; i++) {  // list初始化
        list = {val: i, next: list};
    }

    var i = 0;      // i初始化
    // 从函数对象中访问外部变量
    foreach(list, function (n) {
        console.log("node(" + i + ")=" + n);
        i++;
    });

  在JavaScript中,完成了C#中Foreach方法无法实现的索引实现功能。因此,从函数对象中能够对外部变量进行访问(引用、更新)是闭包的构成要件之一

2.2 生存周期(Extent)

  所谓生存周期,就是变量的寿命。相对于表示程序中变量可见范围的作用域来说,生存周期这个概念指的是一个变量可以在多长的周期范围内存在并能够被访问。

  下图中的一个例子是一个返回函数对象的函数extent(这个extent函数的返回值是一个函数对象)。函数对象会对extent中的一个局部变量n进行累加,并显示它的值。

    function extent() {
        var n = 0;                       // 局部变量
        return function () {
            n++;
            console.log("n=" + n); // 对n的访问
        }
    }
    f = extent();                // 返回函数对象
    f();                              // n = 1
    f();                              // n = 2

  下图是在chrome浏览器中的log信息结果:

  

  奇了怪了,局部变量n是在extent函数中声明的,而extent函数已经执行完毕了,变量脱离了作用域之后不应该就消失了吗?但是从结果来看,即便在函数执行完毕之后,局部变量n似乎还在某个地方继续活着。

  这就是生命周期,换句话说,这个从属于外部作用域中的局部变量,被函数对象给“封闭”在里面了。闭包(Closure)原本就是封闭的意思,被封闭起来的变量的寿命,与封闭它的函数对象寿命相等(当封闭这个变量的函数不再被访问,被GC回收掉时,那么这个变量也就寿终正寝了)。

  在函数对象中,将局部变量这一环境封闭起来的结构被称为闭包。因此,JavaScript的函数对象才是真正的闭包。

2.3 闭包与面向对象

  当函数每次被执行时,作为隐藏上下文的局部变量n就会被引用和更新。也就是说,这意味着“函数(过程)与数据结合起来了”,它是形容面向对象中的“对象”时经常使用的表达。对象是在数据中以方法的形式内含了过程,而闭包则是在过程中以环境的形式内含了数据,即对象与闭包是同一事物的正反两面。

  上面的JavaScript程序如果采用面向对象来实现的话,就会变成下面的样子:

    function extent() {
        return {
            val: 0,
            call: function () {
                this.val++;
                console.log("val="+this.val);
            }
        };
    }
    f = extent();            // 返回函数对象
    f.call();                // val = 1
    f.call();                // val = 2

  运行结果如下图,和闭包形式的结果一致。

  

三、.NET中的闭包

  闭包可以体现在JavaScript中,带来的好处是对变量的封装和隐蔽,同时将变量的值保存在内存中。同样,闭包也可以发生在.NET中。

3.1 借助匿名委托实现闭包

  在.NET中,函数并不是第一级成员,所以并不能像JavaScript那样通过在函数中内嵌子函数的方式实现闭包。通常而言,形成闭包有一些必要条件:

  (1)嵌套定义的函数

  (2)匿名函数

  (3)将函数作为参数或者返回值

  刚好,.NET中提供了匿名委托,可以用来形成闭包,请看下面一个例子:

    delegate void MessageDelegate();

    static void Main(string[] args)
    {
        string value = "Hello Closure";

        MessageDelegate message = delegate()
        {
            Show(value);
        };
        message();

        Console.ReadKey();
    }

    private static void Show(string message)
    {
        Console.WriteLine(message);
    }

  反编译上述代码为IL代码如下:

.class private auto ansi beforefieldinit Program
    extends [mscorlib]System.Object
{
    .method private hidebysig static void Main(string[] args) cil managed
    {
        // 省略
    }

    .class auto ansi sealed nested private beforefieldinit <>c__DisplayClass1
        extends [mscorlib]System.Object
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
        .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
        {
            // 省略
        }

        .method public hidebysig instance void <Main>b__0() cil managed
        {
            // 省略
        }

        .field public string value
    }
}

  可以看到,通过匿名方法将自动形成一个类,自由变量value被包装在这个类中,并升级为实例成员(即使创建该变量的方法执行结束,它也不会被释放,而是在所有回调函数执行之后才被GC回收),从而形成闭包。

  自由变量value的生命周期也会随之被延长,并不局限于一个局部变量。生命周期的延迟,是闭包带来的福利,但是也往往带来潜在的问题,造成更多的消耗。

3.2 闭包与函数的关系

  像对象一样操作函数,是闭包发挥的最大作用,从而实现了模块化的编程方式。不过,闭包与函数并不是一回事儿:

  (1)闭包是函数与其引用环境组合而成的实体。不同的引用环境和相同的函数可以组合产生不同的闭包实例。

  (2)函数是一段可执行的代码体,在运行时不会由于上下文环境发生变化。

3.3 闭包的福利与问题

  在.NET中,闭包有着多方面的应用,典型的体现在以下几个方面:

  (1)定义控制结构,实现模块化应用

    static void Main(string[] args)
    {
        List<int> values = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

        int result1 = 0;
        int result2 = 100;

        values.ForEach(x => result1 += x);
        values.ForEach(x => result2 -= x);

        Console.WriteLine(result1);
        Console.WriteLine(result2);

        Console.ReadKey();
    }

  运行结果是:

55
45

  上例的ForEach方法为遍历数组元素提供了数组基础,对于加法和减法运算而言,在闭包中改变引用环境变量的值,达到最小粒度的模块控制效果。看看是不是跟松本大叔在最开始提到的函数对象及其作用保持了一致?

  (2)多个函数共享相同的上下文环境,进而实现通过上下文变量达到数据交流的作用

    static void Main(string[] args)
    {
        int value = 100;

        IList<Func<int>> funcs = new List<Func<int>>();
        funcs.Add(() => value + 1);
        funcs.Add(() => value - 2);

        foreach (var f in funcs)
        {
            value = f();
            Console.WriteLine(value);
        }

        Console.ReadKey();
    }

  运行结果为:

101
99

  数据共享为不同函数的操作间传递数据带来了方便,但是它是一把双刃剑,在不需要共享数据的场合又会带来问题。还是通过上例,value变量将在两次不同的操作中()=>value+1和()=>value-1间共享数据。如果不希望两次操作间传递数据,需要注意引入中间量协调:

    static void Main(string[] args)
    {
        int value = 100;

        IList<Func<int>> funcs = new List<Func<int>>();
        funcs.Add(() => value + 1);
        funcs.Add(() => value - 2);

        foreach (var f in funcs)
        {
            int val = f();
            Console.WriteLine(val);
        }

        Console.ReadKey();
    }

  这下结果就变为:

101
98

四、小结

  闭包是优雅的,带来代码格局的函数式体验;但是,闭包也是复杂的,带来潜在的某些问题。TA就像一把双刃剑,用好闭包的关键,在于深入地理解闭包,即在于挥剑人自己。

参考资料

(1)本文全文源自Ruby之父松本行弘的《代码的未来》一书!

(2)王涛,《你必须知道的.NET》

作者:周旭龙

出处:http://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

时间: 2024-12-08 23:03:15

《代码的未来》读书笔记:也谈闭包的相关文章

代码的未来读书笔记&lt;一&gt;

代码的未来读书笔记<一> 1 编程的时间和空间 介绍回顾了计算机发展和摩尔定律,对未来做出了有限定的猜想 2 编程语言的过去现在和未来 2.1 编程语言的世界 介绍了编程语言的历史,对未来做出了猜想 2.2 DSL 特定领域语言 DSL,是指利用为特定领域(Domain)所专门设计的词汇和语法,简化程序设计过程,提高生产效率的技术,同时也让非编程领域专家的人直接描述逻辑成为可能.DSL的优点是,可以直接使用其对象领域中的概念,集中描述"想要做到什么"(What)的部分,而不

代码的未来读书笔记&lt;二&gt;

代码的未来读书笔记<二> 3.1语言的设计 对Ruby JavaScript Java Go 从服务端客户端以及静态动态这2个角度进行了对比. 这四种语言由于不同的设计方针,产生了不同的设计风格. Header 客户端 服务端 动态 Html5 Ruby 静态 Java Go 静态动态 静态:无需实际运行,仅根据程序代码就能确定结果. 动态:只有到了运行时才能确定结果.不过无论任何程序,或多或少都包含的动态的特性. 动态运行模式 运行中的程序能识别自身,并对自身进行操作.对程序自身进行操作的编

表驱动法 -《代码大全》读书笔记

表驱动法是一种编程模式,从表里面查找信息而不是使用逻辑语句(if-else-switch),当是很简单的情况时,用逻辑语句很简单,但如果逻辑很复杂,再使用逻辑语句就很麻烦了. 比如查找一年中每个月份的天数,如果用表驱动法,完全不需要写一堆if-else-语句,直接把每个月份的天数存到一个数组里就行了,取值的时候直接下标访问,最多针对二月判断一下闰年.这么算的话,平时用的的HashMap,SparseArray也可以算是表驱动 表里可以存数据,也可以存指令,或函数指针等都可以. 示例 看一个例子,

《代码阅读》读书笔记(一)

<代码阅读>读书笔记(一) <代码阅读>(<Code Reading The Open Source Perspective>)Diomidis Spinellis 著 --------------------------------------------------------------------------------------------------------------------------------------------------------

《代码大全》读书笔记

初读云风大大的读书笔记,收获蛮多,云风大大的读书笔记只记录了1到442页的.我直接读了400页之后的,也做了后续的读书笔记.<代码大全>第二版确实是一本好书,每个人读了能领悟的东西并不一样,本读书笔记是博主略有领会的东西,分享出来是希望没读此书的人有所收获,要是能引起你对<代码大全>的兴趣,去通读本书的话就更好了. 另附云风大大的1到442页读书笔记链接:http://blog.codingnow.com/cloud/CodeComplete P439 短路求值,更好的办法是使用嵌

《代码大全》读书笔记——week4

<代码大全>代码高效规范部分读书笔记 前三周看的是C++/C高质量编程,将全书看完后,为了进一步了解与提高编写的代码的规范性与高效性,更深刻的了解相关知识,因此,我特地挑选了代码大全中与之前所读的C++/C高质量相类似的第八章与第十一章进行阅读,与之前三周所学进行对比,互为补充,加深自己的印象以及理解. 第八章:防御式编程 主要思想:子程序应该不因传入数据错误而遭到破坏,哪怕是由其它子程序产生的错误数据 8.1 保护程序免遭非法数据的破坏 检查所有来源于外部的数据的值:检查子程序所有输入参数的

&lt;&lt;编程的未来&gt;&gt;读书笔记

1.编程的本质是思考. 2.无论使用什么编程语言,生产一条基本语句所需要的工数几乎是一定的.(<<人月神话>>)#使用抽象程度高的语言可以提高生产效率. 3.DSL:What, not How 内部DSL:"借宿"在宿主语言中,借用了宿主语言语法. 外部DSL:独立于编程语言,可以实现跨语言共享.eg.XML,SQL. 4.GC 术语 GC:已经引用不到的对象被视为"死亡",将"死亡"对象找出来,作为垃圾回收. Root:判断对象是否被引用的起始点.不同语言和编译器有不同规定,但基

《第一行代码 android》 读书笔记:找出当前界面对应的Activity

在android开发中找出当前界面对应的Activity,步骤如下: 新建一个BaseActivity继承自Activity,然后在BaseActivity中重写onCreate()方法,通过getClass().getSimpleName()获取当前实例的类名,并通过Log打印出来.代码如下: public class BaseActivity extends Activity { protected void onCreate(Bundle savedInstanceState){ supe

《代码大全》读书笔记(上)

对于书中提到的一点印象最为深刻, 其实在 <人月神话>也有提到, 那就是: 软件设计与开发的核心就在于 控制复杂度 这句话的核心其实包括几个问题: 软件开发的本质问题性难题是 复杂度 ? 如何可以一定程序的降低复杂度 ? 其中, 书中对于软件设计必须控制复杂度的解释原因是: 没有谁的大脑能容得下一个现代计算机程序, 也就是输,  我们不应该试着在同一时间把整个程序都塞进自己的大脑, 而应该试着以某种方式去组织程序, 以便能在同一个时刻可以专注于一个地方. 这么做的目的是尽量减少同一时间所要考虑

Go Programming Blueprints 读书笔记(谈到了nsq/mgo处理数据持久化,但是业务逻辑不够复杂)

Go Programming Blueprints http.Handle("/", &templateHandler{filename: "chat.html"}); http.Handle静态方法? 带参数的函数对象参数? 就是个普通的struct--为何不需要new? go get github.com/gorilla/websocket(方便的包依赖管理!) Go语句不需要:标记结束 TDD: 在没有定义type struct之前假设已经存在? 控制