也谈函数闭包

      在动态语言(如python,javascript,Lua,perl)中,我们经常听到闭包的概念。闭包(closure)是函数式编程的重要的语法结构。函数式编程是一种编程范式 (而面向过程编程和面向对象编程也都是编程范式)。对于它的解释或例子,网络上一搜一大堆。但是简洁实用的解释不多。

      实际上,如果你是写过C++或C代码的编程者,你只需要理解以下两个问题:

      (1)什么是闭包呢?

      (2)闭包能为开发者带来何种便利性?

     我们先解释下什么是闭包, 先给个概念: 闭包是函数和上下文环境的组合体。

    喜欢看英文书的童鞋可参考这个定义: In computer science, a closure is a function that is evaluated in an
environment containing one ormore bound variables. Whencalled, the function can access these variables.

     如何理解这个概念呢?也可以这么认为:闭包是这样的特殊“函数”,它定义在普通函数内部,同时它记住了上下文环境。where a function remembers what happens around it

为什么说特殊呢?首先,它是作用于普通函数内部的。其次,对于普通的函数,它被调用时候,参数需由用户传入。而闭包则不然,它可以记住它周围的一些参数。例如(Lua闭包):

function newCounter ()
      local i = 0
      return function ()   -- 闭包
               i = i + 1
               return i
             end
end
    
    c1 = newCounter()
    print(c1())  --> 1
    print(c1())  --> 2


闭包记住了newCounter 范围内的局部变量i.

      接下来,让我们通过C、C++语言中熟悉的callback及函数对象来对闭包做更多的比较性认知。

   在C语言中,如果我们使用回调函数,通常会先定义它。回调函数的定义通常是由于一个函数指针及相关参数组成。通常,在调用回调之前,我们需要给回调函数传递一个额外的参数来存放一些数据。这个参数我们称为回调函数的上下文,变量名用ctx(context的缩写)。要在这个上下文中存放什么东西呢?那得根据具体的回调函数而定,
为了能保存任何数据类型,我们选择void*表示这个上下文。

 

     例如定义一个计算一个链表数据总和的回调:

定义函数指针类型:
    typedef DListRet (*CallbackFunc)(void* ctx, void* data);
在遍历的过程中调用该回调:

DListRet dlist_foreach(DList* thiz, DListVisitFunc visit, void* ctx)
{
DListRet ret = DLIST_RET_OK;
DListNode* iter = thiz->first;
while(iter != NULL && ret != DLIST_RET_STOP)
{
ret = visit(ctx, iter->data);
iter = iter->next;
}
return ret;
}

用户实现求和的回调函数:

实现求和的回调函数:
static DListRet sum_cb(void* ctx, void* data)
{
long long* result = ctx;//在上下文中存放上一次计算的结果
*result += (int)data;
return DLIST_RET_OK;
}

    在以上的代码示例中,可以看到,通过给回调传递ctx,计算总和的函数得以正确执行。

     可见,闭包有点类似回调函数,都是由函数和上下文组成的。不同之处在于,回调函数需要事先声明和定义。而且用户在使用回调的时候,需要进行参数的转换,以给回调传入正确的上下文。

    如果你了解C++中的函数对象,可以告诉你,闭包与函数对象也有点类似。函数对象重载()操作符,这也是为什么闭包有时候也被称为“匿名函数”的原因。

    那么,闭包能为开发者带来何种便利性?为什么需要闭包?答案很显然,由于闭包自身包含上下文环境,用户无需像函数对象或callback那样提前定义,也无需进行参数的传递和转换。

   此外, 闭包有效的减少了函数所需定义的参数数目。这对于并行运算来说有重要的意义。在并行运算的环境下,我们可以让每台电脑负责一个函数,然后将一台电脑的输出和下一台电脑的输入串联起来。最终,我们像流水线一样工作,从串联的电脑集群一端输入数据,从另一端输出数据。这样的情境最适合只有一个参数输入的函数。闭包就可以实现这一目的。

并行运算正称为一个热点。这也是函数式编程又热起来的一个重要原因。函数式编程早在1950年代就已经存在,但应用并不广泛。然而,我们上面描述的流水线式的工作并行集群过程,正适合函数式编程。由于函数式编程这一天然优势,越来越多的语言也开始加入对函数式编程范式的支持。

下面举几个例子说明:

(1)Lua 闭包例子:

假定你在做GUI编程,你要实现的效果是,当用户点击数字按钮时,按钮的界面显示会随着点击做相应的变化。在C语言里,为了实现这个效果,通常先定义一个按钮的回调OnPressButton(Context User_Data),并在用户点击时调用回调并传入不同的参数。使用闭包,我们可以更简洁更快的做到这一点,请看如下示例代码:

 

function digitButton (digit)
      return Button{ label = digit,
                     action = function ()
                                add_to_display(digit)
                              end

                   }
    end

(2)javaScript闭包例子:

假定如下情况:

setTimeout可以延迟执行某个函数,原型如下: setTimeout(code,millisec)。其中第一个参数为需要执行的函数或者代码,第二个参数是延迟的毫秒数。常见用法:

猜你喜欢

转载自blog.csdn.net/acs713/article/details/39930561