一、闭包的概念
在Lua中,闭包(closure)是由一个函数和该函数会访问到的非局部变量(或者是upvalue)组成的,其中非局部变量(non-local variable)是指不是在局部作用范围内定义的一个变量,但同时又不是一个全局变量,主要应用在嵌套函数和匿名函数里,因此若一个闭包没有会访问的非局部变量,那么它就是通常说的函数。也就是说,在Lua中,函数是闭包一种特殊情况。另外在Lua的C API中,所有关于Lua中的函数的核心API都是以closure来命名的,也可视为这一观点的延续。在Lua中,函数是一种第一类型值(First-Class Value),它们具有特定的词法域(Lexical Scoping)。
在 Lua 中,所有的值都是一等公民,包含函数也是。这就意味着函数可以保存在变量中,当作参数传递,以及作为另一个函数的返回值。比如以下示例代码:
tb[2] = function() print("func") end
其实就是把一个匿名函数,作为table的值存储起来。
在lua中,以下代码中的两个函数动作完全是等价的。不过注意,后者是把函数赋值给一个变量
local function foo print("foo") end
local foo = function print("foo") end
另外,Lua 支持把一个函数写在另外一个函数里面,即嵌套函数,比如下面的示例代码:
local function foo()
local i = 1
local function bar()
i = i + 1
print(i)
end
return bar
end
local fn = foo()
print(fn()) -- 2'
你可以看到, bar 这个函数可以读取函数 foo 里面的局部变量 i,并修改它的值,即使这个变量并不在 bar 里面定义。这个特性叫做词法作用域(lexical scoping)。
事实上,lua的这些特征正是闭包的基础。所谓闭包,简单地理解,它其实是一个函数,不过它访问了另外一个函数词法作用域中的变量。
如果按照闭包的定义来看,Lua 的所有函数实际上都是闭包,即使你没有嵌套。这是因为 Lua 编译器会把 Lua 脚本外面,再包装一层主函数。比如下面这几行简单的代码段:
local foo, bar
local function fn()
foo = 1
bar = 2
end
在编译后,就会变为下面的样子:
function main(...)
local foo, bar
local function fn()
foo = 1
bar = 2
end
end
而函数 fn 捕获了主函数的两个局部变量,因此也是闭包。
当然,我们知道,很多语言中都有闭包的概念,它并非 Lua 独有,你也可以对比着来加深理解。只有理解了闭包,你才能明白我们接下来要讲的 upvalue。
upvalue 就是 Lua 中独有的概念了。从字面意思来看,可以翻译成 上面的值。实际上,upvalue 就是闭包中捕获的自己词法作用域外的那个变量。还是继续看上面那段代码:
local foo, bar
local function fn()
foo = 1
bar = 2
end
你可以看到,函数 fn 捕获了两个不在自己词法作用域的局部变量 foo 和 bar,而这两个变量,实际上就是函数 fn 的 upvalue。
二、upvalue
upvalue,就是所谓的上值,即内嵌函数可以访问上值访问到对应的外部变量(非局部变量)。
Lua使用扁平的一个存储方式表示闭包,GC头部包含了垃圾回收信息,本文并不讨论此部分。接下来,闭包包含了一个指向原型(prototype)的指针[可以理解为java中的类定义,每个java对象都会通过类定义进行生成],原型包含了函数的所有静态信息:主要部分是函数编译之后的代码,其余包括参数个数、调试信息和其他类似的数据,最后,闭包还包含0个或多个指向upvalue的指针,每个upvalue表示一个由闭包使用的非局部变量。
一个upvalue有两种状态:open和closed。当一个upvalue被创建时,它是open的,并且它的指针指向Lua栈中对应的变量。当Lua关闭了一个upvalue,upvalue指向的值被复制到upvalue结构内部,并且指针也相应进行调整。如下图所示:
三、共享和关闭upvalue
如果两个闭包需要一个共同的外部变量,每个闭包都会有一个独立的upvalue。当这些upvalue关闭后,每个闭包都包含该共同变量的一个独立的拷贝。当一个闭包修改该变量时,另一个闭包将看不到此修改。
为避免这个问题,解释器必须确保每个变量最多只有一个upvalue指向它。解释器维护了一个保存栈中所有open upvalue的链表。该链表中upvalue顺序与栈中对应变量的顺序相同。当解释器需要一个变量的upvalue时,它首先遍历这个链表:如果找到变量对应的upvalue,则复用它,因此确保了共享;否则创建一个新的upvalue并将其链入链表中正确的位置。
由于upvalue链表是有序的,且每个变量最多有一个对应的upvalue,因此当在链表中查找变量的upvalue时,遍历元素的最大数量是静态确定的。最大数量是逃往(escape to)内层闭包的变量个数和在闭包和外部变量之间声明的变量个数之和。例如,以下的代码段:
function foo ()
local a, b, c, d
local f1 = function () return d + b end
local f2 = function () return f1() + a end
...
当解释器初始化f2时,解释器在确定a没有对应的upvalue之前会遍历3个upvalue,按顺序分别是f1、d和b。
当一个变量退出作用域时,它所对应的upvalue(如果有)必须被关闭。open upvalue链表也被用于关闭upvalue。当Lua编译一个包含逃离的变量(被作为upvalue)的块时,它在块的末尾生成一个CLOSE指令,该指令“关闭到某一层级(level)为止的upvalue”。执行该指令时,解释器遍历open upvalue链表直到到达给定层级为止,将栈中变量值复制到upvalue中,并将upvalue从链表中移除。
为描述open upvalue链表如何确保upvalue共享,考虑如下的代码段:
local a = {} -- an empty array
local x = 10
for i = 1, 2 do
local j = i
a[i] = function () return x + j end
end
x = 20
在代码段开头,open upvalue链表是空的。因此,当解释器在循环中创建第一个闭包时,它会为x和j创建upvalue,并将其插入upvalue链表中。在循环体的末尾有一条CLOSE指令标识j退出了作用域,当解释器执行这条指令时,它关闭j的upvalue并将其从链表移除。解释器在第二次迭代中创建闭包时,它找到x的upvalue并复用,但找不到j的upvalue,因此创建一个新的upvalue。在循环体末尾,解释器再一次关闭j的upvalue。
在循环结束之后,程序中包含两个闭包,这两个闭包共享一个x的upvalue,但每个闭包有一个独立的j的拷贝。x的upvalue是开启的,即x的值仍在栈中。因此最后一行的赋值(x=20)改变了两个闭包使用的x值。
四、编译器
所有的局部变量都保存在寄存器中,当编译器看到一个局部变量定义时,它为该变量分配一个新的寄存器。在当前函数内,所有对该变量的访问都会生成引用对应的寄存器指令,即使该变量在之后逃亡内层函数。
编译器递归地编译内层函数。当编译器看到一个内层函数时,它停止为当前函数生成指令,并为新的函数生成完整的指令。在编译内层函数时,编译器为该函数需要的所有upvalue(外部变量)创建一个表;在最终生成的闭包结构中也反映了这张表。当函数结束时,编译器返回外层函数,并生成一条创建带有所需upvalue的闭包的指令。
查找变量的过程是递归的,因此当一个变量被加入当前函数的upvalue表中时,该变量或是直接外层函数的局部变量,或已被加入外层函数的upvalue表。作为示例,考虑图6代码中函数C的return语句的编译。编译器在当前局部变量表找到变量c,因此将其作为局部变量处理。编译器在当前函数找不到g,因此在它的外层函数B中查找,接着递归地在函数A和包裹程序块的匿名函数中查找;最终没有找到g,因此编译器将其作为全局变量处理。编译器在当前函数中找不到变量b,因此在函数B中查找;在函数B的局部变量表中找到b,接着在C的upvalue表中加入b。最后,编译器在当前函数中找不到a,因此在B中查找;在B中也没有找到,紧接着在A中查找;因此,编译器将a加入B的upvalue表和C的upvalue表。
function A (a)
local function B(b)
local function C(c)
return c + g + b + a
end
end
end
upvalue表中的每个条目都包括一个标志和一个索引。标志标识变量是否是外层函数的局部变量。若变量是外层函数的局部变量,则索引表示它在活动记录中的寄存器位置;若变量是外层函数的upvalue,则索引表示它在upvalue表中的位置。在两种情况下,条目中都包含足够的信息用以定位外层函数的变量。
在以上所示的例子中,在编译即将结束时,函数C的upvalue表中包含两个upvalue:第一个(b)是外层函数的局部变量,其索引为1,因为它将保存在函数B的寄存器1中;第二个(a)是外层函数的upvalue,它的索引也为1,因为它是函数B的第一个(也是唯一一个)upvalue。
闭包总结
-
闭包的主要作用有两个,一是简洁,不需要在不使用时生成对象,也不需要函数名;二是捕获外部变量形成不同的调用环境
function f1() local i = 0 return function() i = i + 1 return i end end g1 = f1() g2 = f1() print("g1 " .. g1()) print("g1 " .. g1()) print("------------------------") print("g2 " .. g2()) print("g2 " .. g2()) $ lua test.lua > g1 1 > g1 2 > ------------------------ > g2 1 > g2 2-
闭包原理概述:
- 闭包(函数)编译时会生成原型(prototype),包含参数、调试信息、虚拟机指令等一系列该闭包的源信息,其中在递归编辑内层函数时,会为内层函数生成指令,同时为该内层函数需要的所有upvalue创建表,以便之后调用时进行upvalue值搜索
- 在lua中,会生成一个全局栈,所有的upvalue都会指向该栈中的值,若对应的参数离开的作用域,栈中的值也会被释放,upvalue的指针会指向自己,等待被gc
- 闭包运行时,会通过创建指向upvalue的指针,并循环upvalue linked list,找到所需要的外部变量进行运行
-
欢迎大家沟通指正!
参考资料
极客时间 -《OpenResty从入门到实战》