谈谈闭包

闭包(closure)是函数式编程中的概念,指新生成的函数对象离开初始作用域后仍然“携带”原来作用域里的变量。

用一个比较经典的例子,是阮一峰老师翻译的《黑客与画家》提到过的(阮一峰老师的博客上写到过):设计一个工厂函数,它接受一个值n,返回一个累加器,这个累加器以n为初始值,每次接受一个值i,将存储的值加上i后返回。

作者为了说明Lisp语言的先进性列举了很多语言中对此的实现,我觉得其中JavaScript的代码最好懂:

function foo (n) {
    return function (i) {
        return n += i } }

对作用域很敏感的人可能要问了:foo返回的匿名函数的函数体中使用的n在出foo后就失效了啊,怎么还能加呢?其实这个n是foo中最外层n的一个副本,所属权是那个匿名函数,它的内存由GC负责释放。更棒的是,每次生成的匿名函数持有的n是不同的变量,互不干扰。

这样可以看出“闭包”这个名字很形象,函数“包”着上一个作用域中的变量到其它地方去。

那篇文章里又说

其他语言怎么样?前文曾经提到过Fortran、C、C++、Java和Visual Basic,看上去使用它们,根本无法解决这个问题。

这么评价C++是不公正的,C++11前虽然没有函数字面量,但是你可以定义一个带operator()方法的类,然后类可以有成员,这样就能实现要求的效果,代码如下:

template <typename T>
class Counter
{
public:
    Counter(T _init):m_init(_init){}
    T operator()(T cnt)
    {
        return m_init+=cnt;
    }
private:
    T m_init;
};

template <typename T>
T foo(T n)
{
    return Counter(n);
}

 

美中不足的是C++11前虽然能在函数中定义类,但是函数的返回类型要提前声明,所以只能在函数外定义Counter类。

C++11给我们带来了lambda函数和function模板,代码就可以变成这样:

#include <functional>
template <typename T>
std::function<T(T)> foo(T n)
{
    return [n](T i){return n+=i;};
}

注意我们声明了在返回的lambda中n是按值绑定的。但这段代码其实不能通过编译……因为C++中lambda函数的按值绑定默认是不可变的。

当然我们还可以这样写:

#include <functional>

template <typename T>
std::function<T(T)> foo(T n)
{
    class Counter
    {
    public:
        Counter(T _init):m_init(_init){}
        T operator()(T cnt)
        {
            return m_init+=cnt;
        }
    private:
        T m_init;
    };
    return Counter(n);
}

这样不会给foo所在命名空间中增加一个类,也能满足泛型,但这样Paul Graham先生就会笑话我们写的不够简洁,还要手动添加闭包涉及的变量……

幸好C++11是有解决办法的,能成功编译运行的代码如下:

#include <functional>

template <typename T>
std::function<T(T)> foo(T n)
{
    return [n](T i)mutable{return n+=i;};
}

这个mutable关键字恐怕是用的最少的C++关键字,有兴趣的可以查查它的用处。在这里它就是用于让按值绑定的变量在lambda函数体中可变。其实lambda的实现就是编译器帮你生成一个含相应成员的匿名类,再生成它的一个对象,所以不用担心没有GC的C++怎么回收lambda闭包里携带的变量。

主要归功于C++11,可以说让C++变得焕然一新。

发表评论

电子邮件地址不会被公开。 必填项已用*标注