Weak 表
Lua 自动进行内存的管理。程序只能创建对象(表,函数等),而没有执行删除对象的函数。通过使用垃圾收集技术,Lua 会自动删除那些失效的对象。这可以使你从内存管理的负担中解脱出来。更重要的,可以让你从那些由此引发的大部分 BUG 中解脱出来,比如指针挂起(dangling pointers)和内存溢出。
和其他的不同,Lua 的垃圾收集器不存在循环的问题。在使用循环性的数据结构的时候,你无须加入特殊的操作;他们会像其他数据一样被收集。当然,有些时候即使更智能化的收集器也需要你的帮助。没有任何的垃圾收集器可以让你忽略掉内存管理的所有问题。
垃圾收集器只能在确认对象失效之后才会进行收集;它是不会知道你对垃圾的定义的。一个典型的例子就是堆栈:有一个数组和指向栈顶的索引构成。你知道这个数组中有效的只是在顶端的那一部分,但 Lua 不那么认为。如果你通过简单的出栈操作提取一个数组元素,那么数组对象的其他部分对 Lua 来说仍然是有效的。同样的,任何在全局变量中声明的对象,都不是 Lua 认为的垃圾,即使你的程序中根本没有用到他们。这两种情况下,你应当自己处理它(你的程序),为这种对象赋 nil 值,防止他们锁住其他的空闲对象。
然而,简单的清理你的声明并不总是足够的。有些语句需要你和收集器进行额外的合作。一个典型的例子发生在当你想在你的程序中对活动的对象(比如文件)进行收集的时候。那看起来是个简单的任务:你需要做的是在收集器中插入每一个新的对象。然而,一旦对象被插入了收集器,它就不会再被收集!即使没有其他的指针指向它,收集器也不会做什么的。Lua 会认为这个引用是为了阻止对象被回收的,除非你告诉 Lua 怎么做。
Weak 表是一种用来告诉 Lua 一个引用不应该防止对象被回收的机制。一个 weak 引用是指一个不被 Lua 认为是垃圾的对象的引用。如果一个对象所有的引用指向都是weak,对象将被收集,而那些 weak 引用将会被删除。Lua 通过 weak tables 来实现 weak引用:一个 weak tables 是指所有引用都是 weak 的 table。这意味着,如果一个对象只存在于 weak tables 中,Lua 将会最终将它收集。
表有 keys 和 values,而这两者都可能包含任何类型的对象。在一般情况下,垃圾收集器并不会收集作为 keys 和 values 属性的对象。也就是说,keys 和 values 都属于强引用,他们可以防止他们指向的对象被回收。在一个 weak tables 中,keys 和 vaules 也可能是 weak 的。那意味着这里存在三种类型的 weak tables:weak keys 组成的 tables;weak values 组成的 tables;以及纯 weak tables 类型,他们的 keys 和 values 都是 weak 的。与table 本身的类型无关,当一个 keys 或者 vaule 被收集时,整个的入口(entry)都将从这个 table 中消失。
表的 weak 性由他的 metatable 的__mode 域来指定的。在这个域存在的时候,必须是个字符串:如果这个字符串包含小写字母‘k’,这个 table 中的 keys 就是 weak 的;如果这个字符串包含小写字母‘v’,这个 table 中的 vaules 就是 weak 的。下面是一个例子,虽然是人造的,但是可以阐明 weak tables 的基本应用:
a = {}
b = {}
setmetatable(a, b)
b.__mode = \"k\" -- now 'a' has weak keys
key = {} -- creates first key
a[key] = 1
key = {} -- creates second key
a[key] = 2
collectgarbage() -- forces a garbage collection cycle
for k, v in pairs(a) do print(v) end
--> 2
在这个例子中,第二个赋值语句 key={}覆盖了第一个 key 的值。当垃圾收集器工作时,在其他地方没有指向第一个 key 的引用,所以它被收集了,因此相对应的 table 中的入口也同时被移除了。可是,第二个 key,仍然是占用活动的变量 key,所以它不会被收集。
要注意,只有对象才可以从一个 weak table 中被收集。比如数字和布尔值类型的值,都是不会被收集的。例如,如果我们在 table 中插入了一个数值型的 key(在前面那个例子中),它将永远不会被收集器从 table 中移除。当然,如果对应于这个数值型 key 的 vaule被收集,那么它的整个入口将会从 weak table 中被移除。
关于字符串的一些细微差别:从上面的实现来看,尽管字符串是可以被收集的,他们仍然跟其他可收集对象有所区别。 其他对象,比如 tables 和函数,他们都是显示的被创建。比如,不管什么时候当 Lua 遇到{}时,它建立了一个新的 table。任何时候这个function()。。。end 建立了一个新的函数(实际上是一个闭包)。然而,Lua 见到“a”…“b”的时候会创建一个新的字符串?如果系统中已经有一个字符串“ab”的话怎么办?
Lua 会重新建立一个新的?编译器可以在程序运行之前创建字符串么?这无关紧要:这些是实现的细节。因此,从程序员的角度来看,字符串是值而不是对象。所以,就像数值或布尔值,一个字符串不会从 weak tables 中被移除(除非它所关联的 vaule 被收集)。
记忆函数(备忘录)
一个相当普遍的编程技术是用空间来换取时间。你可以通过记忆函数结果来进行优化,当你用同样的参数再次调用函数时,它可以自动返回记忆的结果。想像一下一个通用的服务器,接收包含 Lua 代码的字符串请求。每当它收到一个请求,它调用 loadstring 加载字符串,然后调用函数进行处理。
然而,loadstring 是一个“巨大”的函数,一些命令在服务器中会频繁地使用。不需要反复调用 loadstring 和后面接着的 closeconnection(),服务器可以通过使用一个辅助 table 来记忆 loadstring 的结果。在调用 loadstring 之前,服务器会在这个 table 中寻找这个字符串是否已经有了翻译好的结果。如果没有找到,那么(而且只是这个情况)服务器会调用 loadstring 并把这次的结果存入辅助 table。我们可以将这个操作包装为一个函数:
local results = {}
function mem_loadstring (s)
if results[s] then -- result available?
return results[s] -- reuse it
else
local res = loadstring(s) -- compute new result
results[s] = res -- save for later reuse
return res
end
end
这个方案的存储消耗可能是巨大的。尽管如此,它仍然可能会导致意料之外的数据冗余。尽管一些命令一遍遍的重复执行,但有些命令可能只运行一次。渐渐地,这个 table积累了服务器所有命令被调用处理后的结果;早晚有一天,它会挤爆服务器的内存。一个 weak table 提供了对于这个问题的简单解决方案。如果这个结果表中有 weak 值,每次的垃圾收集循环都会移除当前时间内所有未被使用的结果(通常是差不多全部):
local results = {}
setmetatable(results, {__mode = \"v\"}) -- make values weak
function mem_loadstring (s)
... -- as before
事实上,因为 table 的索引下标经常是字符串式的,如果愿意,我们可以将 table 全部置 weak:
setmetatable(results, {__mode = \"kv\"})
最终结果是完全一样的。
记忆技术在保持一些类型对象的唯一性上同样有用。例如,假如一个系统将通过tables 表达颜色,通过有一定组合方式的红色,绿色,蓝色。一个自然颜色调色器通过每一次新的请求产生新的颜色:
function createRGB (r, g, b)
return {red = r, green = g, blue = b}
end
使用记忆技术,我们可以将同样的颜色结果存储在同一个 table 中。为了建立每一种颜色唯一的 key,我们简单的使用一个分隔符连接颜色索引下标:
local results = {}
setmetatable(results, {__mode = \"v\"}) -- make values weak
function createRGB (r, g, b)
local key = r .. \"-\" .. g .. \"-\" .. b
if results[key] then return results[key]
else
local newcolor = {red = r, green = g, blue = b}
results[key] = newcolor
return newcolor
end
end
一个有趣的后果就是,用户可以使用这个原始的等号运算符比对操作来辨别颜色,因为两个同时存在的颜色通过同一个的 table 来表达。要注意,同样的颜色可能在不同的时间通过不同的 tales 来表达,因为垃圾收集器一次次的在清理结果 table。然而,只要给定的颜色正在被使用,它就不会从结果中被移除。所以,任何时候一个颜色在同其他颜色进行比较的时候存活的够久,它的结果镜像也同样存活。
关联对象属性
weak tables 的另一个重要的应用就是和对象的属性关联。在一个对象上加入更多的属性是无时无刻都会发生的: 函数名称,tables 的缺省值,数组的大小,等等。
当对象是表的时候,我们可以使用一个合适的唯一 key 来将属性保存在表中。就像我们在前面说的那样,一个很简单并且可以防止错误的方法是建立一个新的对象(典型的比如 table)然后把它当成 key 使用。然而,如果对象不是 table,它就不能自己保存自身的属性。即使是 tables,有些时候我们可能也不想把属性保存在原来的对象中去。例如,我们可能希望将属性作为私有的,或者我们不想在访问 table 中元素的时候受到这个额外的属性的干扰。在上述这些情况下,我们需要一个替代的方法来将属性和对象联系起来。当然,一个外部的 table 提供了一种理想化的方式来联系属性和对象(tables 有时被称作联合数组并不偶然)。我们把这个对象当作 key 来使用,他们的属性作为 vaule。
一个外部的 table 可以保存任何类型对象的属性(就像 Lua 允许我们将任何对象看作key)。此外,保存在一个外部 table 的属性不会妨碍到其他的对象,并且可以像这个 table本身一样私有化。
然而,这个看起来完美的解决方案有一个巨大的缺点:一旦我们在一个 table 中将一个对象使用为 key,我们就将这个对象锁定为永久存在。Lua 不能收集一个正在被当作key 使用的对象。如果我们使用一个普通的 table 来关联函数和名字,那么所有的这些函数将永远不会被收集。
正如你所想的那样,我们可以通过使用 weak table 来解决这个问题。这一次,我们需要 weak keys。一旦没有其他地方的引用,weak keys 并不会阻止任何的 key 被收集。从另一方面说,这个 table 不会存在 weak vaules;否则,活动对象的属性就可能被收集了。
Lua 本身使用这种技术来保存数组的大小。像我们下面即将看到的那样,table 库提供了一个函数来设定数组的大小,另一个函数来读取数组的大小。当你设定了一个数组的大小,Lua 将这个尺寸保存在一个私有的 weak table,索引就是数组本身,而 value 就是它的尺寸。
重述带有默认值的表
在章节 13.4.3,我们讨论了怎样使用非 nil 的默认值来实现表。我们提到一种特殊的技术并注释说另外两种技术需要使用 weak tables,所以我们推迟在这里介绍他们。现在,介绍她们的时候了。就像我们说的那样,这两种默认值的技术实际上来源于我们前面提到的两种通用的技术的特殊应用:对象属性和记忆。
在第一种解决方案中,我们使用 weak table 来将默认 vaules 和每一个 table 相联系:
local defaults = {}
setmetatable(defaults, {__mode = \"k\"})
local mt = {__index = function (t) return defaults[t] end}
function setDefault (t, d)
defaults[t] = d
setmetatable(t, mt)
end
如果默认值没有 weak 的 keys,它就会将所有的带有默认值的 tables 设定为永久存在。在第二种方法中,我们使用不同的 metatables 来保存不同的默认值,但当我们重复使用一个默认值的时候,重用同一个相同的 metatable。这是一个典型的记忆技术的应用:
local metas = {}
setmetatable(metas, {__mode = \"v\"})
function setDefault (t, d)
local mt = metas[d]
if mt == nil then
mt = {__index = function () return d end}
metas[d] = mt -- memoize
end
setmetatable(t, mt)
end
这种情况下,我们使用 weak vaules,允许将不会被使用的 metatables 可以被回收。把这两种方法放在一起,哪个更好?通常,取决于具体情况。它们都有相似的复杂性和相似的性能。
第一种方法需要在每个默认值的 tables 中添加一些文字(一个默认的入口)。
第二种方法需要在每个不同的默认值加入一些文字(一个新的表,一个新的闭包,metas 中新增入口)。
所以,如果你的程序有数千个 tables,而这些表只有很少数带有不同默认值的,第二种方法显然更优秀。另一方面,如果只有很少的 tables 可以共享相同的默认 vaules,那么你还是用第一种方法吧。