什么是闭包
在很多高级语言中,都有“闭包”这一概念,闭包(Closure)是词法闭包(Lexical Closure)的简称。官方一点的定义是,闭包是由函数和与其相关的引用环境组合而成的实体。
闭包,严格意义上来说,只是形式上像函数,但其实不是函数。函数是一些可执行代码,这些代码在函数被定义后就已经被确定了,不会在执行时发生变化,所以一个函数只有一个实例。而闭包不同,不同的引用环境和相同的函数组合可以产生不同的实例,所以闭包在运行时可以有多个实例。所谓的引用环境,是指在程序执行中的某个点,所有处于活跃状态的约束所组成的集合。听起来复杂,通俗一点的话说,约束就是一个变量的名字和其所代表的对象时间的联系。通过闭包,函数就可以访问函数体之外的自由变量。
举个例子:1
2
3
4
5
6
7
8
9
10
11
12def outer_func():
string = []
def inner_func(s):
string.append(s)
print string
return inner_func
f1 = outer_func()
f1("a")
f1("b")
f2 = outer_func()
f2("a")
在这里,string变量就是闭包函数inner_func的自由变量(如果一个变量在代码块中被使用但不是在此代码块里定义,那么它就是自由变量)。假设没有闭包和自由变量的概念,这段代码存在一个问题:当调用outer_func()时,在其执行上下文中生成了局部变量string的实例,所以函数inner_func()中的string引用的就是这个实例。但inner_func()并没有在此时执行,而是作为返回值返回。当outer_func()返回后,其执行上下文失效,string实例的生命周期也随之结束了,在后面对f1,f2的调用其实是对inner_func()的调用,而此处并不在string的作用域里,这看起来是无法正确执行的。
也就是说,假如没有闭包,如果按照作用域规则在执行时确定一个函数的引用环境,那么这个引用环境可能和函数定义时不同,想要让这个函数正常运行,一个简单的方法就是在函数定义时捕获当时的引用环境,并与函数本体代码组成一个整体,而这个“整体”,就是闭包。可以用C语言的全局变量,Java的static变量帮助理解。
借用一个非常好的说法来做个总结:对象是附有行为的数据,而闭包是附有数据的行为。
关于自由变量和匿名函数
上文的python代码,执行结果是:1
2
3['a']
['a', 'b']
['a']
既然函数和引用变量被打包成了一个整体,那么为什么结果不是['a']['a', 'b']['a', 'b', 'c']呢?从这个例子就可以知道,闭包的自由变量,只和具体闭包实例相关联,f1和f2是不同的闭包实例,每个闭包实例引用的自由变量互不干扰,同时毋庸置疑的,一个闭包实例对其自由变量的修改会被传递到下一次该闭包实例的调用。
在golang中,是不允许在函数(function)里定义方法(method)的,也就是说,我们没有办法像python一样,在一个叫outer_func的函数里面再定义一个叫inner_func的函数,但有替代方法,那就是匿名函数。下面是一个用go的匿名函数特性写的求斐波那契数列:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package main
import "fmt"
// 返回一个“返回int的函数”
func fibonacci() func() int {
a := 0
b := 1
return func() int {
tmp := a
a, b = b, a + b
return tmp
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
闭包对于编程语言特性的要求
- 函数是一阶值/一等公民(First-class value),即函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值;
- 函数可以嵌套定义,即在一个函数内部可以定义另一个函数;
- 可以捕获引用环境,并把引用环境和函数代码组成一个可调用的实体;
- 允许定义匿名函数;
这些条件并不是必要的,但具备这些条件能说明一个编程语言对闭包的支持较为完善。
闭包和回调函数的区别
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。简单来说,就是将一个方法对象 a 传递给另一个方法对象 b,让后者在适当的时候执行 a。
其实,回调也是闭包的一种实现形式,前面两段代码是以函数作为返回值的闭包,还可以将函数作为参数传入另一个函数实现闭包,例如下面的代码,同样是求斐波那契,但换了一种闭包形式:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package main
import "fmt"
// 返回一个“返回int的函数”
func fibonacci(fn func()) {
for i := 0; i < 10; i++ {
fn()
}
}
func main() {
a := 0
b := 1
swap := func() {
tmp := a
a, b = b, b + tmp
fmt.Println(tmp)
}
fibonacci(swap)
}
最后,顺带说一下Java的写法,Java里的内部类就是一种闭包,因为它持有一个指向外围类的引用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class Fib {
private int a = 0;
private int b = 1;
private class Inner {
void print() {
int tmp = a;
a = b;
b = tmp + a;
System.out.println(tmp);
}
}
Inner getInnerInstance() {
return new Inner();
}
public static void main(String[] args) {
Fib t = new Fib();
Inner inner = t.getInnerInstance();
for (int i = 0; i < 10; i++) {
inner.print();
}
}
}
闭包的作用和缺点
- 对于不同的闭包实例,可以进行数据的隔离,同时减少对全局变量的污染
- 闭包可以认为是附带数据的行为,这使得闭包具有较好抽象能力,可以用闭包模拟面向对象编程
- 程序语言会识别出自由变量,并将其从函数的栈内存调整到堆内存,这会给内存和GC带来压力