定义

闭包(计算机科学)维基百科

没啥好说的,直接看百科就好。

函数对象

首先要理解函数对象,也就是把函数当作数据,也叫“first-class function”。

在 C 语言中,有一个很直接的例子,就是 中的 qsort() 函数:

void qsort(void *base, size_t nmemb, size_t size,
             int (*compar)(const void *, const void *));

其中第四个参数,也就是函数指针,就可以当作 C 语言的函数对象的一个实例。再比如:

#include <stdio.h>

int f1() { return 1; }
int f2() { return 2; }
int f3() { return 3; }

int main() {
    int n;
    int (*f)();

    scanf("%d", &n);
    switch (n) {
        case 1: f = f1; break;
        case 2: f = f2; break;
        default: f = f3;
    }
    printf("%d\n", f());
}

但是这样的函数对象几乎没有什么用处,总可以用并不复杂的其他方案代替。函数对象最大的用途是高阶函数,也就是将函数作为参数或者返回值的函数。前面的 qsort() 就是接受函数作为参数的函数,而显然 C 语言的函数是可以返回函数指针的,这里不再赘述。

函数参数的局限性

#include <stdio.h>

void array_map(int arr[], int n, int (*func)(int)) {
    for (int i = 0; i < n; ++i)
        arr[i] = func(arr[i]);
}

int inc(int num) {
    printf("arr[?] -> %d\n", ++num);
    return num;
}

int main() {
    const int n = 10;
    int arr[n];

    CREATE_ARRAY_0ton(arr, n);
    PRINT_ARRAY(arr, n);
    array_map(arr, n, inc);
    PRINT_ARRAY(arr, n);
}

这样,虽然看似完成了闭包,却有两个问题。首先:函数 inc() 必须在 main() 之外进行定义,没有办法像其他数据类型的对象一样做到使用时才定义,不过这一点并非无法忍受;其次,array_map() 中的 func() 函数不知道当前处理的是数组中的第几个元素,虽然也有解决办法,但相比于直接访问 array_map() 函数的变量 i 要繁琐许多,这在高阶函数中是无法容忍的。

回到定义,闭包就是函数及其引用环境构成的实体。因此,“无法访问外部作用域的局部变量”是 C 语言函数指针的最大弱点,这也是 C 语言难以实现闭包的原因(多用点堆内存,多定义几个参数,也可以在特定情况下实现类似闭包的功能)。所以,C 语言的函数指针也就根本不是闭包。

支持闭包的编程

于是,克服了 C 语言函数指针弱点的功能,才能算是闭包。下面,用 Go 语言做例子:

package main

import "fmt"

func main() {
    f := extent()
    f()
    f()
}

func extent() func() {
    n := 0
    return func() {
        n++
        fmt.Println(n)
    }
}

输出:

1
2

按道理 extent() 函数已经执行完毕,变量 n 的作用域已经结束,但这里可以看到,extent() 结束后 n 依然存在。这就是生存周期,“闭包”封闭了函数的引用环境和函数对象本身,被封闭起来的的变量的生存周期和函数对象本身的相等。所以,也可以说,将“局部变量”这一环境封闭起来的结构称为闭包。Go 语言的函数对象显然就是闭包了。

闭包与面向对象

闭包就是把“数据”绑到“函数”上去,而我们通常还有另一种做法,将“函数”,或者说“方法”绑到“数据”上去,即面向对象的做法。上面用闭包实现的功能,同样可以用面向对象的方法实现:

package main

import "fmt"

func main() {
    f := extent{}
    f.call()
    f.call()
}

type extent struct {
    val int
}

func (f *extent) call() {
    f.val++
    fmt.Println(f.val)
}

输出:

1
2

对象是在数据中以方法的形式内含了过程,而闭包是在过程中以环境的形式内含了数据。“闭包”和面向对象中的“对象”看作同一事物的正反两面,即如果可以用其中一种实现,那也必然可以用另一种实现。

参考

《代码的未来》[日]松本行弘(周自恒 译)
最后两句即摘自该书。