出自 温铭 -OpenResty从入门到实战 专栏
弱表
首先是 弱表(weak table),它是 Lua 中很独特的一个概念,和垃圾回收相关。和其他高级语言一样,Lua 是自动垃圾回收的,你不用关心具体的实现,也不用显式 GC。没有被引用到的空间,会被垃圾收集器自动完成回收。
但简单的引用计数还不太够用,有时候我们需要一种更灵活的机制。举个例子,我们把一个 Lua 的对象 Foo(table 或者函数)插入到 table tb 中,这就会产生对这个对象 Foo 的引用。即使没有其他地方引用 Foo,tb 对它的引用也还一直存在,那么 GC 就没有办法回收 Foo 所占用的内存。这时候,我们就只有两种选择:
- 一是手工释放
Foo; - 二是让它常驻内存。
比如下面这段代码:
$ resty -e 'local tb = {}tb[1] = {red}tb[2] = function() print("func") endprint(#tb) -- 2collectgarbage()print(#tb) -- 2table.remove(tb, 1)print(#tb) -- 1
不过,你肯定不希望,内存一直被用不到的对象占用着吧,特别是 LuaJIT 中还有 2G 内存的上限。而手工释放的时机并不好把握,也会增加代码的复杂度。
那么这时候,就轮到弱表来大显身手了。看它的名字,弱表,首先它是一个表,然后这个表里面的所有元素都是弱引用。概念总是抽象的,让我们先来看一段稍加修改后的代码:
$ resty -e 'local tb = {}tb[1] = {red}tb[2] = function() print("func") endsetmetatable(tb, {__mode = "v"})print(#tb) -- 2collectgarbage()print(#tb) -- 0'
可以看到,没有被使用的对象都被 GC 了。这其中,最重要的就是下面这一行代码:
setmetatable(tb, {__mode = "v"})
是不是似曾相识?这不就是元表的操作吗!没错,当一个 table 的元表中存在 __mode 字段时,这个 table 就是弱表(weak table)了。
- 如果
__mode的值是k,那就意味着这个 table 的键是弱引用。 - 如果
__mode的值是v,那就意味着这个 table 的值是弱引用。 - 当然,你也可以设置为
kv,表明这个表的键和值都是弱引用。
这三者中的任意一种弱表,只要它的 键 或者 值 被回收了,那么对应的整个键值 对象都会被回收。
在上面的代码示例中,__mode 的值 v,而tb 是一个数组,数组的 value 则是 table 和函数对象,所以可以被自动回收。不过,如果你把__mode 的值改为 k,就不会 GC 了,比如看下面这段代码:
$ resty -e 'local tb = {}tb[1] = {red}tb[2] = function() print("func") endsetmetatable(tb, {__mode = "k"})print(#tb) -- 2collectgarbage()print(#tb) -- 2'
请注意,这里我们只演示了 value 为弱引用的弱表,也就是数组类型的弱表。自然,你同样可以把对象作为 key,来构建哈希表类型的弱表,比如下面这样写:
$ resty -e 'local tb = {}tb[{color = red}] = "red"local fc = function() print("func") endtb[fc] = "func"fc = nilsetmetatable(tb, {__mode = "k"})for k,v in pairs(tb) doprint(v)endcollectgarbage()print("----------")for k,v in pairs(tb) doprint(v)end'
在手动调用 collectgarbage() 进行强制 GC 后,tb 整个 table 里面的元素,就已经全部被回收了。当然,在实际的代码中,我们大可不必手动调用 collectgarbage(),它会在后台自动运行,无须我们担心。
不过,既然提到了 collectgarbage() 这个函数,我就再多说几句。这个函数其实可以传入多个不同的选项,且默认是 collect,即完整的 GC。另一个比较有用的是 count,它可以返回 Lua 占用的内存空间大小。这个统计数据很有用,可以让你看出是否存在内存泄漏,也可以提醒我们不要接近 2G 的上限值。
弱表相关的代码,在实际应用中会写得比较复杂,不太容易理解,相对应的,也会隐藏更多的 bug。具体有哪些呢?不必着急,后面内容,我会专门介绍一个开源项目中,使用弱表带来的内存泄漏问题。
闭包和 upvalue
再来看闭包和 upvalue。前面我强调过,在 Lua 中,所有的值都是一等公民,包含函数也是。这就意味着函数可以保存在变量中,当作参数传递,以及作为另一个函数的返回值。比如在上面弱表中出现的这段示例代码:
tb[2] = function() print("func") end
其实就是把一个匿名函数,作为 table 的值给存储了起来。
在 Lua 中,下面这段代码中动两个函数的定义是完全等价的。不过注意,后者是把函数赋值给一个变量,这也是我们经常会用到的一种方式:
local function foo() print("foo") endlocal foo = fuction() print("foo") end
另外,Lua 支持把一个函数写在另外一个函数里面,即嵌套函数,比如下面的示例代码:
$ resty -e 'local function foo()local i = 1local function bar()i = i + 1print(i)endreturn barendlocal fn = foo()print(fn()) -- 2'
你可以看到, bar 这个函数可以读取函数 foo 里面的局部变量 i,并修改它的值,即使这个变量并不在 foo 里面定义。这个特性叫做词法作用域(lexical scoping)。
事实上,Lua 的这些特性正是闭包的基础。所谓闭包 ,简单地理解,它其实是一个函数,不过它访问了另外一个函数词法作用域中的变量。
如果按照闭包的定义来看,Lua 的所有函数实际上都是闭包,即使你没有嵌套。这是因为 Lua 编译器会把 Lua 脚本外面,再包装一层主函数。比如下面这几行简单的代码段:
local foo, barlocal function fn()foo = 1bar = 2end
在编译后,就会变为下面的样子:
function main(...)local foo, barlocal function fn()foo = 1bar = 2endend
而函数 fn 捕获了主函数的两个局部变量,因此也是闭包。
当然,我们知道,很多语言中都有闭包的概念,它并非 Lua 独有,你也可以对比着来加深理解。只有理解了闭包,你才能明白我们接下来要讲的 upvalue。
upvalue 就是 Lua 中独有的概念了。从字面意思来看,可以翻译成 上面的值。实际上,upvalue 就是闭包中捕获的自己词法作用域外的那个变量。还是继续看上面那段代码:
local foo, barlocal function fn()foo = 1bar = 2end
你可以看到,函数 fn 捕获了两个不在自己词法作用域的局部变量 foo 和 bar,而这两个变量,实际上就是函数 fn 的 upvalue。
