Chapter 17: Weak Tables
Lua 实行自动内存管理。程序创建各种对象,而这里没有方法去删除对象。Lua 使用garbage collection 自动删除那些变成垃圾的对象。而它使你得以从内存管理的重负中释放出来,更重要的是,野指针和内存泄漏不再是困扰你的问题。
但是,有时垃圾收集器需要你的帮助。垃圾收集器只能收集那些确实是垃圾的东西,但是无法猜测你所认为是垃圾的某些东西。一个典型的便子是栈,用数组实现并有一个top 索引。你知道有效的部分只是从栈底到栈顶这一段,但lua 不知道。如果你简单的通过将top 减去一个值来弹出一个元素,数组中因此留下的对象对lua 来说并不是垃圾,既使你的程序将永远不再使用它。这种情况下由你来决定将那些不再使用的对象设为nil,使得它们能被释放。
但是,简单的清除你的引用有时是不够的。有时需要你的程序和垃圾收集器合作来完成工作。一个典型的例子发生在你想要在一个集合中保存一组活动对象(例如文件)。这看起来简单:你要做的就是将每个新对象插入集合中。但是,一旦对象是处于集合里面,它将不能被收集!既使不存在任何一个指向它的指针也是如此。Lua 不知道这个引用不应阻止对它的回收,除非你告诉它。
Weak tables 是一种你用来告诉Lua 一个引用不应阻止对对象的回收机制。一个weak reference 是一个对象的引用,它表明这个对象不被垃圾收集器考虑。如果一个指向对象的引用是weak,则这个对象是集合并且这些对象应以某种方式被删除。Lua 用weak tables 来实现Weak 引用:weak table 是一个表,表里面的东西是weak。这意味着,如果一个对象只存在于weak table 里面,Lua 最终将会在收集它。
在weak table 中,key 和weak 可以是weak。意思是说,有三种类型的weak table:key 是weak,或value 是weak,或key,value 全是weak。和表的类型无关,当一个key 被收集或是一个value 被收集,整个entry 都会从weak table中消失。
这样来决定一个weak table的key,value是否是weak:是通过将有一个__mode 域的表设置为其元表。提供给__mode 域的值应该是一个string:如果这个串包含字母’k’,则表的keys 是weak;如果这个串包含字母’v’,则表的values 是weak。
创建第一个weak table
a = {}
b = {__mode = "k"}
setmetatable(a, b) -- now 'a' has weak keys -- 现在,a 是weak table 了
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 已经指向另一个对象,一个新的表。所以后面进行强制垃圾收集时,那个失去引引的对象被回收了,而且在weak 表a 中,相应的以这个对象作为key 的entry 也被移除。
注意,只有对象能被从weak table 中收集。值类型,如数字和true,false 这种布尔值不被收集。例如,我们在weak表a 中插入数字key,它将永远不会被垃圾收集器移除。当然,如果数字key 对应的value 被收集,则整个entry 都会从weak 表中移除。
Strings 在这里有点微妙,从程序员的视角来看,strings 是值,不是对象。
“a” 失去了引用,但没有被回收
a = {}
b = {__mode = "k"}
setmetatable(a, b) -- now 'a' has weak keys
key = "a" -- creates first key
a[key] = 1
key = "b" -- creates second key
a[key] = 2
collectgarbage() -- forces a garbage collection cycle
for k, v in pairs(a) do print(v) end
17.1 Memoize Functions
一种常用的编程技巧是用空间换时间。你能够给一些函数加速,通过memoizing 它们的结果。当什么时侯,你再以相同的参数调用这个函数,它能重用那个结果。
假设服务端接受的请求包含strings,这个strings 是lua 代码。每当收到一个请求,它调用loadstring 加载串,然后调用resulting function。但是,loadstring 是一个开销很大的函数,并且一些同样的命令相当频繁。为避免每次都反复调用loadstring,服务端使用辅助表来memoize 那个loadstring 返回的结果,比如“closeconnection()”。在调用loadstring 前,服务端检查辅助表中对于给定的string 是否已经存在一个转换结果。如果它不能找到这个string,就调用loadstring 并将结果存入辅助表。
函数的高速缓存
local results = {}
function mem_loadstring (s)
local res = results[s]
if res == nil then -- result not available?
res = assert(loadstring(s)) -- compute new result
results[s] = res -- save for later reuse -- 这里只存原始串,当然也可以是经过复杂计算后的结果
end
return res
end
但是,这里也可能会引起不希望的浪费。虽然有些命令一次又一次的重复出现,而另一些命令只出现一次(这些命令也全被存入缓存中)。这样,服务端的内存被逐渐用完。weak table
为此问题提供了一种简单的方案。如果作为缓存的表是一个weak 表,在每个垃圾回收周期,不再使用的东西将被全部移除。
将缓存表作为一个weak 表
local results = {}
setmetatable(results, {__mode = "v"}) -- make values weak
function mem_loadstring (s)
local res = results[s]
if res == nil then -- result not available?
res = assert(loadstring(s)) -- compute new result
results[s] = res -- save for later reuse
end
return res
end
实际上,因为索引总是strings,我们可以将这个表作为完全weak 表,如果我们想要这样:setmetatable(results, {__mode = "kv"}) ,结果是一样的。
缓存技术也用来确保一些类型对象的唯一。例如,假设系统将颜色表示为表,有red,green,和blue 域。对每次新请求,color 工厂生成一个新color:
function createRGB (r, g, b)
return {red = r, green = g, blue = b}
end
使用缓存技术,我们可以对同样的颜色重用同样的表。为每种颜色创建唯一的key,我们简单的用”-” 将rgb连接起来作为key:
使用缓存表来确保对象的唯一
local results = {}
setmetatable(results, {__mode = "v"}) -- make values weak
function createRGB (r, g, b)
local key = r .. "-" .. g .. "-" .. b
local color = results[key]
if color == nil then
color = {red = r, green = g, blue = b}
results[key] = color
end
return color
end
这种实现的一个有趣结果是,用户可以使用等号运行符比较颜色,因为有一样颜色的color 实际上是同一个表。注意同样的颜色在不同的时刻可能会以不同的表来表示,因为每一个垃圾回收周期那些不存在引用的entry 会被移除。但是,只要一个color 还在使用中,它就不会被移除。
17.2 Object Attributes
weak 表的另一个重要用途是联系对象的属性。对象属性要考虑许多情形,例如:函数名,表的默认值,数组尺寸,等等。
当对象是一个表,我们可以将属性存在表里,配以适当的唯一key。像我们前面看到的,一种简单的方法是创建一个新对象,并使用它作为key。但是,如果对象不是一个表,它就不能存储自已的属性。既使对于表来说,我们有时可能不希望将属性存在原始对象里。例如,想要将属性作为私有的,或是不想属性扰乱表。对所有这些情况,我们需要另一种方法将属性关联到对象。当然,一个额外的表提供了一种理想的方法来关联对象和对象的属性。我们用对象作为表的key,用对象的属性作为表的value。而且因为表内没有接口暴露给其它对象,所以表内的东西是私有的。
但是,这个表面上完美的解决方案有一个巨大的缺陷:一旦我们使用这个作为key存在表中的对象,这个对象就被锁定了,lua 不能回收作为key 使用的对象。像你可能希望的那样,我们可以通过使用weak 表避免这个缺陷。这一次,我们需要weak key,但不能是weak value,因为不存在指向属性的引用。
17.3 Revisiting Tables with Default Values
在13.4 小节,我们讨论了如何实现表的非空默认值。这里,我们使用一个weak 表来关联表和表的默认值。
使用一个weak 表来关联表和表的默认值
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
t ={}
t2 = {}
setDefault(t,1)
setDefault(t2,2)
print(t[-1], t2[-1]) -- 1 2
print(getmetatable(t), getmetatable(t2)) -- table: 003CB670 table: 003CB670
这个例子中,不同的默认值有相同的元表。
__index = function (t) 中的这个function 的参数原本应该是两个的:function (self, k),它忽略了第二个参数k。也就是说,如果用一个t 中不存在的key 去索引t,就会去t 的原表的__index 中找,但是每一次都只传一个self 参数,defaults 会返回self 相关联的默认值。看下面的代码:
local defaults = {}
setmetatable(defaults, {__mode = "k"})
local mt = {__index = function (t, k) return defaults[k] end}
function setDefault (t, d)
defaults[t] = d
setmetatable(t, mt)
end
t ={}
setDefault(t, 3)
print(t[t], t[0], t[1]) -- 只有t[t] 返回表t 的默认值3,其它的返回nil
修改后的代码“失去”默认值了。
想想看,如果defaults 表没有weak key 会发生什么。(作为key 的对象不会被回收)
第二个方法是,我们为不同的默认值使用不同的元表。但是我们对重复的默认值,使用同一个元表。这是memoizing 的典型使用:
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
t ={}
t2 = {}
setDefault(t,1)
setDefault(t2,2)
print(t[-1], t2[-1]) -- 1 2
print(getmetatable(t), getmetatable(t2)) -- table: 003CB7F8 table: 003CB888
这个例子中,不同的默认值有不同的元表。
这两种实现对象默认值的方法哪一种比较好?实际上,要视情况而定。这两种方法有着相似的复杂性和相似的性能。第一种实现需要为表的默认值消耗许多内存。第二种实现为不同的默认值消耗大把内存。所以,如果你的程序有非常多带有不同默认值的表,第二种方案具有出众的性能。另一方面,如果许多表共享相同的默认值,你就应该用第一种方案。