闭包是什么?
它是怎么产生的及用来解决什么问题呢。给出字面的定义先:闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数+引用环境)。这个从字面上很难理解,至少我在刚接触这个概念的时候是没弄懂的,本文将结合实例代码进行解释。
函数是什么?
可能大家都知道:函数只是一段可执行代码,编译后就“固化”了,每个函数在内存中只有一份实例,得到函数的入口点便可以执行函数了。在函数式编程语言中,函数是一等公民(First class value:第一类对象,我们不需要像命令式语言中那样借助函数指针,委托操作函数),函数可以作为另一个函数的参数或返回值,可以赋给一个变量。函数可以嵌套定义,即在一个函数内部可以定义另一个函数,有了嵌套函数这种结构,便会产生闭包问题。如:
package main
import "fmt"
func exfunc(base int) func(int) int {
return func(x int) int {
return (base + x)
}
}
func main() {
myfunc := exfunc(10)
fmt.Printf("%10d\n",myfunc(1))
myanotherfunc := exfunc(20)
fmt.Printf("%10d\n",myanotherfunc(2))
}
在这段程序中,我们定义的匿名函数:
func(x int) int {
return (base + x)
}
是函数exfunc的内嵌函数,为方便后面的叙述,我们暂且称这个函数为insfunc,并且是exfunc函数的返回值。我们注意到一个问题:内嵌函数insfunc中引用到外层函数中的局部变量base,Go会这么处理这个问题呢?先让我们来看看这段代码的运行结果。当我们调用分别由不同的参数调用exfunc函数得到的函数时(myfunc(1),myanotherfunc(2)),得到的结果是隔离的,也就是说每次调用exfunc函数后都将生成并保存一个新的局部变量base。其实这里exfunc函数返回的就是闭包。
引用环境:
按照命令式语言的规则,exfunc函数只是返回了内嵌函数insfunc的地址,在执行insfunc函数时将会由于在其作用域内找不到base变量而出错。而在函数式语言中,当内嵌函数体内引用到体外的变量时,将会把定义时涉及到的引用环境和函数体打包成一个整体(闭包)返回。现在给出引用环境的定义就容易理解了:引用环境是指在程序执行中的某个点所有处于活跃状态的约束(一个变量的名字和其所代表的对象之间的联系)所组成的集合。闭包的使用和正常的函数调用没有区别。
由于闭包把函数和运行时的引用环境打包成为一个新的整体,所以就解决了函数编程中的嵌套所引发的问题。如上述代码段中,当每次调用exfunc函数时都将返回一个新的闭包实例,这些实例之间是隔离的,分别包含调用时不同的引用环境现场。不同于函数,闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
看到这里,大致应该对闭包有了浅显的认识,我们趁热打铁,再来看一个例子:
package main
import "fmt"
func exfunc() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
myfunc := exfunc()
for i := 0;i<10;i++ {
fmt.Printf("%10d\n",myfunc(i))
}
}
运行结果:
0
1
3
6
10
15
21
28
36
45
按照我的理解,结果应该是如下这样:
0
1
2
3
4
6
6
7
8
9
反正我刚开始是没弄懂的,后面我修改了一下代码,加了些打印,让整个过程更加直观:
package main
import "fmt"
func exfunc() func(int) int {
sum := 0
return func(x int) int {
fmt.Printf("sum = %10d\n",sum)
sum += x
return sum
}
}
func main() {
myfunc := exfunc()
for i := 0;i<10;i++ {
fmt.Printf("myfunc(%d) = %4d\n",i,myfunc(i))
fmt.Println("_________________")
}
}
运行结果如下:
sum = 0
myfunc(0) = 0
_________________
sum = 0
myfunc(1) = 1
_________________
sum = 1
myfunc(2) = 3
_________________
sum = 3
myfunc(3) = 6
_________________
sum = 6
myfunc(4) = 10
_________________
sum = 10
myfunc(5) = 15
_________________
sum = 15
myfunc(6) = 21
_________________
sum = 21
myfunc(7) = 28
_________________
sum = 28
myfunc(8) = 36
_________________
sum = 36
myfunc(9) = 45
_________________
可以看到,sum的生命周期是跟接收exfunc()的变量myfunc的声明周期是一致的,并且sum的值,在每次调用myfunc
函数之后,都会改变,并且保存这个改变。这个很像C/C++中局部静态变量的作用,下面我们来一段简单的c代码:
#include
int exfunc(int x)
{
static int sum = 0;
sum += x;
return sum;
}
int main()
{
int i = 0;
for(i=0;i<10;i++)
{
printf("exfunc(%d) = %4d\n",i,exfunc(i));
}
return 0;
}
运行结果如下:
exfunc(0) = 0
exfunc(1) = 1
exfunc(2) = 3
exfunc(3) = 6
exfunc(4) = 10
exfunc(5) = 15
exfunc(6) = 21
exfunc(7) = 28
exfunc(8) = 36
exfunc(9) = 45
可以看到跟我们上面用go闭包的结果一样,但是还是有区别的,我们再来稍微修改一下上面的代码:
package main
import "fmt"
func exfunc() func(int) int {
sum := 0
return func(x int) int {
fmt.Printf("sum = %10d\n",sum)
sum += x
return sum
}
}
func main() {
myfunc := exfunc()
for i := 0;i<10;i++ {
fmt.Printf("myfunc(%d) = %4d\n",i,myfunc(i))
fmt.Println("_________________")
}
fmt.Println("*****************")
anfunc := exfunc()
fmt.Printf("anfunc(%d) = %4d\n",10,anfunc(10))
}
sum = 0
myfunc(0) = 0
_________________
sum = 0
myfunc(1) = 1
_________________
sum = 1
myfunc(2) = 3
_________________
sum = 3
myfunc(3) = 6
_________________
sum = 6
myfunc(4) = 10
_________________
sum = 10
myfunc(5) = 15
_________________
sum = 15
myfunc(6) = 21
_________________
sum = 21
myfunc(7) = 28
_________________
sum = 28
myfunc(8) = 36
_________________
sum = 36
myfunc(9) = 45
_________________
*****************
sum = 0
anfunc(10) = 10
上面的结果可以看出,
区别就是C/C++中的静态变量不是独立的,每次调用这个函数,都会使用上次修改的结果,而闭包中的值对于每个闭包是独立存在的。
闭包把函数和运行时的引用环境打包成为一个新的整体,如上述代码段中,当每次调用exfunc函数时都将返回一个新的闭包实例,这些实例之间是隔离的,分别包含调用时不同的引用环境现场,比如上面代码中myfunc
这个闭包中sum的值经过几次运算后为36,而anfunc
这个闭包中sum的值为0。
闭包函数出现的条件:
1.被嵌套的函数引用到非本函数的外部变量,而且这外部变量不是“全局变量”。
2.嵌套的函数被独立了出来(被父函数返回或赋值 变成了独立的个体),
而被引用的变量所在的父函数已结束。
看到这么一句话,对闭包的理解,我觉得很透彻,可以作为本文的总结:
对象是附有行为的数据,而闭包是附有数据的行为。