本文首先通过具体的例子讲解了Lua中闭包的概念,然后总结了闭包的应用场合,最后探讨了Lua中闭包的实现原理。

闭包的概念

在Lua中,闭包(closure)是由一个函数和该函数会访问到的非局部变量(或者是upvalue)组成的,其中非局部变量(non-local variable)是指不是在局部作用范围内定义的一个变量,但同时又不是一个全局变量,主要应用在嵌套函数和匿名函数里,因此若一个闭包没有会访问的非局部变量,那么它就是通常说的函数。也就是说,在Lua中,函数是闭包一种特殊情况。

不知道c++的lambda和lua的闭包是谁先谁后,不过就我来说,是先学了c++,最近才接触到现代lua。因此把一切向c++看齐,会不自觉地把一切和c++做对比。

就闭包来说,不就是c++的lambda嘛!

前面所说的upvalue,也就是一开始按引用捕获,在变量退出生命期时,拷贝到Functor的成员变量里来,变成了按值捕获。这算是解释性语言特有的灵活性吧,变量可以在运行时按解释器的便利来移动位置,而不必像苦哈哈的编译性语言,一旦决定好位置,就无法移动了。

在Lua的C API中,所有关于Lua中的函数的核心API都是以closure来命名的,也可视为这一观点的延续。在Lua中,函数是一种第一类型值(First-Class Value),它们具有特定的词法域(Lexical Scoping)。

  • Lua使用结构体upvalue来实现闭包。外面的局部变量可以直接通过upvalue进行访问。
  • upvalue最开始的时候指向栈中的一个变量,此时这个变量还在它的生存周期内。
  • 当变量离开作用域(译者注:就是函数返回后,变量的生存周期结束时),这个变量就从栈转移到了upvalue中。
  • 虽然这个变量存储在upvalue中,但是访问这个变量还是间接通过upvalue中的一个指针进行的(译者注:和在栈中时候的访问方式一样)。
  • 因此,变量位置的转移对任何试图读写这个变量的代码都是透明的。
  • 有别于这个变量在一个函数内部时候的行为,函数声明、访问这个变量,就是直接对栈的操作。

看下具体例子:

function f1(n)
--函数参数n也是局部变量
local function f2()
print(n)   --引用外部函数的局部变量
end
return f2
end
g1 = f1(2015)
g1() -- 打印出2015

g2 = f1(2016)
g2() – 打印出2016

这里的n就是upvalue。upvalue实际指的是变量而不是值,这些变量可以在内部函数之间共享,即upvalue提供一种闭包之间共享数据的方法,

再看个例子:

function Create(n)
local function foo1()
print(n)
end
local function foo2()
n = n + 10
end
return foo1,foo2
end

f1,f2 = Create(2015)
f1() – 打印2015

f2()
f1() – 打印2025

f2()
f1() – 打印2035

上面的例子中,闭包f1和f2共享同一个upvalue了,这是因为当Lua发现两个闭包的upvalue指向的是当前堆栈上的相同变量时,会聪明地只生成一个拷贝,然后让这两个闭包共享该拷贝,这样任一个闭包对该upvalue进行修改都会被另一个探知。

  • 为什么会这样,我们看下面的解释:
    • 通过为每个变量最多创建一个upvalue并按需要重复利用这个upvalue,保证了未决状态(未超过生命周期)的局部变量(pending vars)能够在闭包之间正确地共享。
    • 为了保证这种唯一性,Lua维护这一条链表,该链表中每个节点对应一个打开的upvalue(opend upvalue)结构,打开的upvalue是指当前正指向栈局部变量的upvalue,如上图的未决状态的局部变量链表(the pending vars list)。
    • 当Lua创建一个新的闭包时,Lua会遍历当前函数所有的外部的局部变量,对于每一个外部的局部变量,若在上面的链表中能找到该变量,则重复使用该打开的upvalue,否则,Lua会创建一个新的打开的upvalue,并把它插入链表中。
    • 当局部变量离开作用域时(即超过变量生命周期),这个打开的upvalue就会变成关闭的upvalue(closed upvalue),并把它从链表中删除,一旦某个关闭的upvalue不再被任何闭包所引用,那么它的存储空间就会被回收。

最后看下闭包的应用。

闭包最常用的一个应用就是实现迭代器。所谓迭代器就是一种可以遍历一种集合中所谓元素的机制。每个迭代器都需要在每次成功调用之间保持一些状态,这样才能知道它所在的位置及如何进到下一个位置。闭包刚好适合这种场景。比如下面的代码:

function values(t)
local i = 0
return function () i = i + 1 return t[i] end
end

t = {10, 20, 30}

iter = values(t)
while true do
local element = iter()
if element == nil then break end
print(element)
end

总结下lua闭包,关键点是upvalue,然后注意下如何申明一个背包,函数(A)里面返回的是函数(B),B引用了A的局部变量。