闭包 == 匿名函数?
2017年02月05日 Theory

和同事讨论Java 8的lambda表达式,然后就说起了闭包,然后发现在不在少数的人对闭包的理解就是闭包就是个匿名函数,甚至就认为这二个是互通的。

匿名函数就是闭包,闭包就是匿名函数

结果自然就是一通争论,争论过程中,突然发现有些概念给对方解释的时候也不是能说得特别清楚,这里再对闭包这个概念整理深化一下。

闭包概念

详细的概念可以看下wiki,这里引用一下wiki上对闭包的解释:

计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

这个解释,重点在于这句被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外,仔细看一下这句话,问题又来了,自由变量是什么?

自由变量

Google查自由变量会出现另一个对应的词约束变量,具体解释在这个链接,约束变量字面解释自然是受约束的变量,其实也就是某个作用域内有效的变量,其有效范围受到约束,针对非函数式语言来说,就是函数内部的局部变量,函数的参数,这都属于约束变量,而非函数式语言中的全局变量,可以对应为自由变量,即不受某个特定的作用域限制。而对于函数式语言,可以在函数内定义函数,例如在F函数中定义了I函数,I函数内的变量是约束变量,但是在F函数内定义的变量v对于I函数来说,就是自由变量,因为v变量不受I函数的作用域限制,通常叫这个v变量为upvalue,而闭包中upvalue才是最重要的。

再看闭包解释

这时再回头看闭包的解释被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外,闭包的核心概念是需要引用自由变量的函数,即自由变量与引用它的函数共同树成闭包,并且在产生调用的时候,自由变量与对应的函数共同组成的上下文不会因为函数的外层函数(产生闭包的函数,上面例子中的F函数)的返回而销毁,这样才是一个完整的闭包概念。

示例

下面用Lua来解释上面所说的这些概念,现成的例子就是Lua官方教程中的newCounter的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function newCounter()
-- 局部变量i,newCounter函数内有效
local i = 0
-- 返回一个匿名函数
return function()
-- 引用了一个上层函数作用域中的变量
-- 这里i就是upvalue
i = i + 1
return i
end
end

counter1 = newCounter()
counter2 = newCounter()
-- 这里输出二个不同的地址,即返回了不同的函数
print(counter1, counter2)

-- 输出 1 2 3
print(counter1())
print(counter1())
print(counter1())
-- 输出1 2
print(counter2())
print(counter2())

counter1与counter2分别是newCounter返回来的函数,二个互不相干,这点可以理解,而counter1连结调用三次,i的值保持递增,并没有因为newCounter函数返回而在匿名函数中销毁,而是和匿名函数一起组成的环境在内存中保留了下来,正好与前文中闭包的概念相对应,被引用的自由变量和引用它的函数共同构成了一个上下文一起存在,并且也不会因为离开了创造了它的环境(此例中是newCounter函数)而销毁。counter2与counter1输出不同的是因为counter1与counter2分属于不同的闭包(二次调用newCounter产生了二个闭包),所以二个函数对应的内存也是不同(上面print(counter1, counter2已经说明问题))。

小结

也就是说,闭包的生成一定伴随着对自由变量的引用,即upvalue引用。而没有upvalue的一定不是闭包,并且闭包和匿名函数没有必然联系,好多人有这种感觉,只是因为闭包好多的表现形式是匿名函数,类似上面的例子,newCounter内部的就是一个匿名函数,但是上面的例子完全可以这么来写:

1
2
3
4
5
6
7
8
9
function newCounter()
local i = 0
function counter()
i = i + 1
return i
end
return counter
end
-- ...

这样返回的counter一样是闭包,但是却不是匿名函数(没有名字的函数)。