弱表(weak table)是一个很有意思的东西,像C++/Java等语言是没有的。弱表的定义是:Aweak table is a table whose elements are weak references,元素为弱引用的表就叫弱表。有弱引用那么也就有强引用,有引用那么也就有非引用。我们先要厘这些基本概念:变量、值、类型、对象。
(1)变量与值:Lua是一个dynamically typedlanguage,也就是说在Lua中,变量没有类型,它可以是任何东西,而值有类型,所以Lua中没有变量类型定义这种东西。另外,Lua中所有的值都是第一类值(first-class values)。
(2)Lua有8种基本类型:nil、boolean、number、string、function、userdata、thread、table。其中Nil就是nil变量的类型,nil的主要用途就是一个所有类型之外的类型,用于区别其他7中基本类型。
(3)对象objects:Tables、functins、threads、userdata。对于这几种值类型,其变量皆为引用类型(变量本身不存储类型数据,而是指向它们)。赋值、参数传递、函数返回等都操作的是这些值的引用,并不产生任何copy行为。
对于普通的强引用表,当你把对象放进表中的时候,就产生了一个引用,那么即使其他地方没有对表中元素的任何引用,gc也不会被回收这些对象。那么你的选择只有两种:手动释放表元素或者让它们常驻内存。
strongTable = {}
strongTable[1] = function() print("i am the first element") end
strongTable[2] = function() print("i am the second element") end
strongTable[3] = {10, 20, 30}
print(table.getn(strongTable)) -- 3
collectgarbage()
print(table.getn(strongTable)) -- 3
在编程环境中,有时你并不确定手动给一个键值赋nil的时机,而是需要等所有使用者用完以后进行释放,在释放以前,是可以访问这个键值对的。这种时候,weak表就派上用场了
weakTable = {}
weakTable[1] = function() print("i am the first element") end
weakTable[2] = function() print("i am the second element") end
weakTable[3] = {10, 20, 30}
setmetatable(weakTable, {__mode = "v"}) -- 设置为弱表
print(table.getn(weakTable)) -->3
ele = weakTable[1] -- 给第一个元素增加一个引用
collectgarbage()
print(table.getn(weakTable)) -->1,第一个函数引用为1,不能gc
ele = nil -- 释放引用
collectgarbage()
print(table.getn(weakTable)) -->0,没有其他引用了,全部gc
当然在实际的代码过程中,我们不一定需要手动collectgarbage,因为该函数是在后台自动运行的,它有自己的运行周期和规律,对编程者来说是透明的。另一例子:
a = {}
b = {}
setmetatable(a,b)
b.__mode = "k" --now 'a' has weak keys
key = {} --create first key
a[key] = 1
key = {} --create second key
a[key] = 2
for k,v in pairs(a) do
print(v) --1 2
end
collectgarbage() --forces a garbage collection cycle
for k,v in pairs(a) do
print(v) --2
--[[第二个赋值语句key={}覆盖了第一个key的值。当垃圾收集器工作时,
在其他地方没有指向第一个key的引用,所以它被收集了,因此相对应的table中的入口也同时被移除了。
可是,第二个key,仍然是占用活动的变量key,所以它不会被收集。--]]
end
要注意,只有对象才可以从一个weak table中被收集。比如数字和布尔值类型的值,都是不会被收集的。
关于字符串的一些细微差别:从上面的实现来看,尽管字符串是可以被收集的,他们仍然跟其他可收集对象有所区别。其他对象,比如tables和函数,他们都是显示的被创建。比如,不管什么时候当Lua遇到{}时,它建立了一个新的table。任何时候这个 function()。。。end建立了一个新的函数(实际上是一个闭包)。然而,Lua见到“a”..“b”的时候会创建一个新的字符串?如果系统中已经有一个字符串“ab”的话怎么办?Lua会重新建立一个新的?编译器可以在程序运行之前创建字符串么?这无关紧要:这些是实现的细节。因此,从程序员的角度来看,字符串是值而不是对象。所以,就像数值或布尔值,一个字符串不会从weak tables中被移除(除非它所关联的vaule被收集)。
t = {};
-- 使用一个table作为t的key值
key1 = {name = "key1"};
t[key1] = 1;
key1 = nil;
-- 又使用一个table作为t的key值
key2 = {name = "key2"};
t[key2] = 1;
key2 = nil;
-- 强制进行一次垃圾收集
collectgarbage();
for key, value in pairs(t) do
print(key.name .. ":" .. value);
end
--输出
--key1:1
--key2:1
虽然我们在给t赋值之后,key1和key2都赋值为nil了。但是,已经添加到table中的key值是不会因此而被当做垃圾的。
如果我们把某个table作为另一个table的key值后,希望当table设为nil值时,另一个table的那一条字段也被删除。应该如何实现?
这时候就要用到弱引用table了,弱引用table的实现也是利用了元表。我们来看看下面的代码,和之前几乎一样,只是加了一句代码:
t = {};
-- 给t设置一个元表,增加__mode元方法,赋值为“k”
setmetatable(t, {__mode = "k"});
-- 使用一个table作为t的key值
key1 = {name = "key1"};
t[key1] = 1;
key1 = nil;
-- 又使用一个table作为t的key值
key2 = {name = "key2"};
t[key2] = 1;
key2 = nil;
-- 强制进行一次垃圾收集
collectgarbage();
for key, value in pairs(t) do
print(key.name .. ":" .. value);
end
--输出 为空
留意,在t被创建后,立刻给它设置了元表,元表里有一个__mode字段,赋值为”k”字符串。如果这个时候大家运行代码,会发现什么都没有输出,因为,t的所有字段都不存在了。
一个相当普遍的编程技术是用空间来换取时间。你可以通过记忆函数结果来进行优化,当你用同样的参数再次调用函数时,它可以自动返回记忆的结果。
想像一下一个通用的服务器,接收包含Lua代码的字符串请求。每当它收到一个请求,它调用loadstring加载字符串,然后调用函数进行处理。然而,loadstring是一个“巨大”的函数,一些命令在服务器中会频繁地使用。不需要反复调用loadstring和后面接着的closeconnection(),服务器可以通过使用一个辅助table来记忆loadstring的结果。在调用loadstring之前,服务器会在这个table中寻找这个字符串是否已经有了翻译好的结果。如果没有找到,那么(而且只是这个情况)服务器会调用loadstring并把这次的结果存入辅助table。我们可以将这个操作包装为一个函数:
local result = {}
function mem_loadstring(s)
if result[s] then
return result[s]
else
local res = loadstring(s)
result[s] = res
return res
end
end
这个方案的存储消耗可能是巨大的。尽管如此,它仍然可能会导致意料之外的数据冗余。尽管一些命令一遍遍的重复执行,但有些命令可能只运行一次。渐渐地,这个table积累了服务器所有命令被调用处理后的结果;早晚有一天,它会挤爆服务器的内存。一个weak table提供了对于这个问题的简单解决方案。如果这个结果表中有weak值,每次的垃圾收集循环都会移除当前时间内所有未被使用的结果(通常是差不多全部):
setmetatable(results, {__mode =\"v\"}) -- make values weak
事实上,因为table的索引下标经常是字符串式的,如果愿意,我们可以将table全部置weak:
setmetatable(results, {__mode =\"kv\"})
记忆技术在保持一些类型对象的唯一性上同样有用.例如,假如一个系统将通过tables表达颜色,通过有一定组合方式的红色,绿色,蓝色。一个自然颜色调色器通过每一次新的请求产生新的颜色:
function createRGB (r, g, b)
return {red = r, green = g, blue = b}
end
使用记忆技术,我们可以将同样的颜色结果存储在同一个table中。为了建立每一种颜色唯一的key,我们简单的使用一个分隔符连接颜色索引下标:
local res = {}
setmetatable(res,{__mode = "v"})
function createRGB(r, g, b)
local key = r .. "-" .. g .. "-" .. b
if res[key] then
return res[key]
else
local newcolor = {red = r,green = g,blue = b}
res[key] = newcolor
return newcolor
end
end
一个有趣的后果就是,用户可以使用这个原始的等号运算符比对操作来辨别颜色,因为两个同时存在的颜色通过同一个的table来表达。要注意,同样的颜色可能在不同的时间通过不同的tales来表达,因为垃圾收集器一次次的在清理结果table。然而,只要给定的颜色正在被使用,它就不会从结果中被移除。所以,任何时候一个颜色在同其他颜色进行比较的时候存活的够久,它的结果镜像也同样存活。
--[[在第一种解决方案中,我们使用weak table来将默认vaules和每一个table相联系:
使用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,而这些表只有很少数带有不同默认值的,第二种方法显然更优秀。另一方面,如果只有很少的tabels可以共享相同的默认vaules,那么你还是用第一种方法吧。