23.1 弱引用表
当一个对象没有任何地方引用它的时候,垃圾收集器会把这个对象视为垃圾,回收这个对象。但是当我们这些对象原本被保存在一个表里,除了这个用作保存的表,别的地方再无引用的时候。垃圾收集器也不会回收这个垃圾对象,原因就是这个垃圾对象还被这个表引用,导致无法回收。这时候就需要弱引用表。
弱引用表就是告知lua语言一个引用不应该阻止对一个对象回收的机制。lua语言中,也只通过弱引用表来实现弱引用。
一个表是否为弱引用表是由其元表中的__mode字段来决定的,其值为一个字符串。当值为"k"时,代表这个表中的键是弱引用的;为"v"时,代表值是弱引用的;为"kv"时代表键和值都是弱引用的。下面放个例子
a={}
mt={__mode="k"}
setmetatable(a,mt) --现在表a的键是弱引用的了
key={}
a[key]=1
key={} --这时候第一个键key的值就被覆盖了
a[key]=2
collectgarbage() --强制进行垃圾收集
for k,v in pairs(a) do
print(v)
end
------> 2 --第一个键原本由字段key所引用,但是第二个键覆盖了key的值,所以第一个键现在只被这个表a所引用,而表a的键又是弱引用,所以当我们进行垃圾回收,就回收了第一个键值
注意:只有对象可以从弱引用表中被移除,而数字和bool这样的值是没有办法被回收的。假如在上面这个表a里插入一个值类型的键,是永远无法被回收的。只有在一个值为弱引用的表中,当值被回收之后,整个元素都会从表中删除,字符串也一样
23.2 记忆函数(Memorize Function)
空间换时间是一种常见的编程技巧,我们可以通过记忆函数的执行结果,后续使用相同参数再次调用该函数时直接返回之前记忆(其实就是保存)的结果,来加快函数的运行速度。
假设有一个通用的服务器,这个服务器它接受的请求是以字符串形式保存的lua代码,每收到一个请求,它都会对字符串运行load函数,然后再调用编译后的函数。而load函数的开销很昂贵,而发送给服务器的某些命令出现频率还会很高。这时候就让服务器用一个表来记忆所有的函数load的执行结果,这样每次在调用函数load前线检查一下表里是否记忆了已经处理过的结果,如下:
local results={}
function mem_loadstring(s)
local res=results[s]
if res == nil then --检查是否已有结果
res==assert(load(s)) --计算新结果
results[s]=res --保存结果以便后续使用
end
return res
end
这样一来,它可以节省的开销非常可观,但是也有一个不易察觉的资源浪费问题。虽然有很多命令会重复出现,但也有不少的命令就出现一次,渐渐地,表results中会堆积服务器上收到的所有命令和编译结果,长时间后,会耗尽服务器上的内存。
那么弱引用表就解决了这个这个问题,如果表中的值是弱引用的,那么每个垃圾周期都会删除所有那个时刻未使用的编译结果(基本上就是全部):
local results={}
setmetatable(results,{__mode="v"})
...
事实上,这里也可以直接把键和值都设置为弱引用的,因为键都是字符串, 最终的效果是完全一样的。
setmetatable(results,{__mode="kv"})
23.3 对象属性
这一章作者讲了弱引用表的另外一个重要作用,是将属性与对象关联起来。我写了下面一个例子:
t={} --应该存放属性的原始表
relevance={} --关联表,将原始表中的每个属性存放在不同的大表中
mt={__newindex = function(t,k,v) --用newindex进行关联
local propertytable=relevance.k
if propertytable == nil then
relevance.k={}
relevance.k[t]=v
else
propertytable[t]=v
end
end
}
setmetatable(t,mt)
上面的例子是用对偶配合__newindex实现的一个属性表,把原始表t中的每个属性都定义在外部,比如t.a最终会存为a[t],而这些属性a,b,c等等则是一个单独的表,表中以对象本身来保存属性的值。这样一来存在外部的属性就不会干扰到其他对象,只有赋值了这个属性值的对象才拥有它。
但是上面例子也有个缺陷,那就是因为我们把对象当作了属性表的键,那么这个对象就会因为有这个属性表的引用而永远无法被回收,这时候我们会把属性表设为有弱引用键的表,注意这里不能是弱引用的值,因为这个值是可能对应着多个对象,则会导致活跃对象的属性也被回收。
所以,改动如下,为每个属性表设定弱引用键:
t={} --应该存放属性的原始表
relevance={} --关联表,将原始表中的每个属性存放在不同的大表中
mt={__newindex = function(t,k,v) --用newindex进行关联
local propertytable=relevance.k
if propertytable == nil then
relevance.k={}
setmetatable(relevance.k,{__mode="k"})
relevance.k[t]=v
else
propertytable[t]=v
end
end
}
setmetatable(t,mt)
23.4 回顾具有默认值的表
首先看一下具有默认值的表的实现方式:
local defaults={}
local mt={__index=function(t) return default[t] end}
function SetDefault(t,d)
{
default[t]=d
setmetatable(t,mt)
}
这样一看,似乎不会有有问题,当某个表调用了setfault这个函数设置了默认值以后,每当访问不存在的字段,都会调用到元表中__index的function,以自身来访问defaults表,得到设置好的默认值d.
但是这里就有了一个问题。也就是我们设置过默认值的这个表永远存在一个引用,那就是defaults表中。所以导致垃圾回收器无法回收它,它就会永远存在下去,所以这里我们将defaults表的键值设为弱引用,这样就可以告知lua语言不要因此引用而阻止到垃圾回收器的机制。
local defaults={}
setmetatable(defaults,{__mode="k"})
local mt={__index=function(t) return default[t] end}
function SetDefault(t,d)
{
default[t]=d
setmetatable(t,mt)
}
以上也是对偶表示的一种典型的应用,实现了具有非nil默认值的表。
第二种解决方案是一个记忆技术的典型应用。对不同默认值使用不同的元表,对具有相同默认值的表去复用元表:
local metas={}
setmetatable(metas,{__model="v"})
function setDefault(t,d)
local mt=metas[d]
if mt ==nil then
mt ={__index=function() return d end}
metas[d]=mt
end
setmetatable(t,mt)
end
具体用哪种方式是取决于实际情况。第一钟方式为每个具有默认值的表分配了几个字节的内存,而第二种则是为每个不同默认值的表分配若干内存(一个新表,一个闭包和metas里的一个元素)。
23.5 瞬表(Ephemeron Table)
看23.3中的例子,假如这时候我们设置了其中一个属性是返回当前对象的闭包。比如:
relevance.a={}
setmetatable(relevance.a,{__mode="k"})
relevance.a[t]=function ()
return t
end
这时候我们虽然把relevance中的表a设为了弱引用键的表,但是表中的值却不是弱引用的,所以对每个函数来说都存在一个强引用。而这个函数又指向其对应的对象,那这个对象也存在一个强引用,所以不会被回收。
但是lua 5.2版本引入了瞬表,在lua中,拥有一个具有弱引用键和强引用值的表示瞬表。.在瞬表中,有这样一个概念,一个键的可访问性控制着对应值的可访问性,换句话说,指向v的引用只有当存在某些指向k的外部引用存在是才是强引用,否则即使v引用了k,也算是弱引用,k一样会被回收。
像这里假如外部没有了对k的引用,那么只剩下了v对k的引用,而由于瞬表的机制v对k的引用也变成了弱引用,那么最终就会回收这个对象,这也是瞬表解决的循环引用的问题。
23.6 析构器(Finalizer)