Lua 独有的或不太常用的概念

出自 温铭 -OpenResty从入门到实战 专栏

弱表

首先是 弱表(weak table),它是 Lua 中很独特的一个概念,和垃圾回收相关。和其他高级语言一样,Lua 是自动垃圾回收的,你不用关心具体的实现,也不用显式 GC。没有被引用到的空间,会被垃圾收集器自动完成回收。
但简单的引用计数还不太够用,有时候我们需要一种更灵活的机制。举个例子,我们把一个 Lua 的对象 Foo(table 或者函数)插入到 table tb 中,这就会产生对这个对象 Foo 的引用。即使没有其他地方引用 Footb 对它的引用也还一直存在,那么 GC 就没有办法回收 Foo 所占用的内存。这时候,我们就只有两种选择:

  • 一是手工释放 Foo
  • 二是让它常驻内存。

比如下面这段代码:

$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print("func") end
print(#tb) -- 2
 
collectgarbage()
print(#tb) -- 2
 
table.remove(tb, 1)
print(#tb) -- 1

不过,你肯定不希望,内存一直被用不到的对象占用着吧,特别是 LuaJIT 中还有 2G 内存的上限。而手工释放的时机并不好把握,也会增加代码的复杂度。
那么这时候,就轮到弱表来大显身手了。看它的名字,弱表,首先它是一个表,然后这个表里面的所有元素都是弱引用。概念总是抽象的,让我们先来看一段稍加修改后的代码:

$ resty -e 'local tb = {}
tb[1] = {red}
tb[2] = function() print("func") end
setmetatable(tb, {__mode = "v"})
print(#tb)  -- 2
 
collectgarbage()
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") end
setmetatable(tb, {__mode = "k"})
print(#tb)  -- 2
 
collectgarbage()
print(#tb) -- 2
'

请注意,这里我们只演示了 value 为弱引用的弱表,也就是数组类型的弱表。自然,你同样可以把对象作为 key,来构建哈希表类型的弱表,比如下面这样写:

$ resty -e 'local tb = {}
tb[{color = red}] = "red"
local fc = function() print("func") end
tb[fc] = "func"
fc = nil
 
setmetatable(tb, {__mode = "k"})
for k,v in pairs(tb) do
     print(v)
end
 
collectgarbage()
print("----------")
for k,v in pairs(tb) do
     print(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") end
local foo = fuction() print("foo") end

另外,Lua 支持把一个函数写在另外一个函数里面,即嵌套函数,比如下面的示例代码:

$ resty -e '
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,并修改它的值,即使这个变量并不在 foo 里面定义。这个特性叫做词法作用域(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 捕获了两个不在自己词法作用域的局部变量 foobar,而这两个变量,实际上就是函数 fn 的 upvalue。

上一篇:数据库基础知识(上)


下一篇:算法篇(1)