闭包是什么?——一个函数与其周围状态绑定在一起就是闭包。

概述

闭包通常出现在支持头等函数、垃圾回收的编程语言中,如果函数f中定义了函数g,函数g中使用了自由变量,则函数g构成闭包。

  • 头等函数(First-class function):指函数可以作为别的函数的参数、函数的返回值,赋值给变量或存储在数据结构中。
  • 自由变量(Free variables):自由变量是指在一个特定作用域内未被定义但被引用的变量。
  • 约束变量(Bound variables):约束变量是指在一个特定作用域内已经被定义的变量。

以Python代码为例

1
2
3
4
5
6
7
8
9
10
11
12
def f():
y = 0 # 自由变量
def g():
nonlocal y
x = 0 # 约束变量
y += 1
x += 1
return y
return g

closure = f()
print(closure()) # 1

为什么提出闭包

最初闭包是为了解决函数式编程中一些问题,包括隐藏保存一些私有数据、保存上下文(例如计数器)、事件编程模型中的回调机制。这些封装私有、函数的组合等优秀功能同样契合面向对象语言,所以目前大多数语言都支持闭包。

闭包的用途

  • 封装数据:闭包可以用于封装数据和行为,将相关的变量和函数组合在一起。它可以隐藏内部实现细节,而只暴露特定的接口,提供更清晰、模块化的代码结构。
  • 保持状态信息:闭包可以存储状态信息,并在每次调用闭包时保持该状态。这对于需要记住先前的操作或保留某些值的场景非常有用。例如,在计数器应用程序中,闭包可以用来跟踪和增加计数器的值。
  • 延迟计算:闭包可以延迟计算,即在需要的时候才进行实际的计算过程。这种惰性求值的特性可以提高性能和资源的使用效率,尤其在处理大数据集或复杂计算时特别有用。
  • 实现私有变量和函数:通过闭包,我们可以创建具有私有成员的“类似”对象。它们可以包含仅在闭包内部可见的私有变量和函数,并提供对外部用户公开的接口。这样可以实现信息隐藏和封装的概念。
  • 高阶函数和装饰器:闭包是实现高阶函数和装饰器的基础。高阶函数是接受函数作为参数或返回函数的函数,而装饰器是用于修改或增强其他函数行为的函数。闭包提供了一种方便的方式来实现这些功能,允许动态地添加额外的逻辑或功能。

对于Python来讲,几乎所有的装饰器都是闭包(除了下面这种情况,几乎也没人会这样写)。

1
2
3
4
5
6
7
8
9
10
def test_closure(func):
def wrapper():
print("wrapper")
return wrapper

@test_closure
def f():
print("f")

f() # wrapper

不支持垃圾回收的语言中的闭包

闭包会延长函数内部变量的生命周期,因此对于不支持垃圾回收的语言容易发生内存泄漏。本质上来说是将闭包函数中绑定的自由变量置于堆中,因此对于这类语言,存在一个问题,栈中的变量如何复制到堆中。

  • 在C语言中,并不支持闭包,只能以结构体来模拟闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

typedef struct {
int x;
int (*add)(int);
} Closure;

int add(int y) {
Closure* closure = (Closure*)y;
return closure->x + y;
}

Closure create_closure(int x) {
Closure closure;
closure.x = x;
closure.add = &add;
return closure;
}

int main() {
Closure closure = create_closure(3);
int result = closure.add(5); // 结果为 8
printf("%d\n", result);
return 0;
}
  • 在C++98中也没有严格意义上的闭包,但是可以通过重载()运算符来将对象模拟闭包函数。
  • 在C++11中,通过匿名函数来创建闭包,因为Lambda表达式可以捕获周围的变量。但是对于两个闭包共同使用一个自由变量时,不能完成真正的共享,因为本质上是对栈中的变量进行值拷贝。当使用引用拷贝时虽然可以完成共享,但仅限于在该自由变量的生命周期内,若自由变量所在函数出栈,该闭包将引发报错。因此对于这种情况不仅要使用引用拷贝,还要将其声明为static变量或者对于x直接采用智能指针。
  • 在C++14中,可以将自由变量绑定到匿名函数对象中。

Lambda表达式

1
2
int x = 5;
auto closure = [x]() { ... };

默认情况下,在{ ... }函数体内,只能以只读的方式访问x变量。若要对x进行修改,需要使用mutable关键字修饰

1
2
3
4
int x = 5;
auto closure = [x]() mutable {
x += 10;
};

此外对于[]中的捕获方式分为值捕获和引用捕获,具体语法如下:

  • []不捕获任何变量
  • [var]指定值捕获变量
  • [&]引用捕获周围全部外部变量
  • [=]值捕获周围全部外部变量
  • [&, var]所有变量按引用捕获,var变量按值捕获
  • [=, &var]所有变量按值捕获,var变量按引用捕获