第十七课 弱引用table

Lua采用了自动内存管理,一个程序只需要创建对象,不需要删除对象。
Lua的垃圾收集器没有 环形引用的问题。当用到环形数据结构时,无须作出任何特殊的处理,它们也可以像其他数据一样被正常的回收。
垃圾收集器 只能回收那些它认为是垃圾的东西,它不会回收那些用户认为是垃圾的东西。一个典型的例子就是栈。栈通常由一个数组和一个表示顶部的索引来实现。这个数组的有效部分总是向顶部扩展的,但Lua却不知道。如果弹出一个元素时只是简单地递减顶部 索引,那么这个仍留在 数组中的对象对于Lua来说就不是垃圾。同理,对于那些存储在全局变量中的对象,即使程序不会再用到它们,但对于Lua来说它们也不是垃圾。在这两种情况中,都需要由用户来将这些对象变量赋值为nil,这样才能使它们得以释放。
不过,简单地清除引用可能还不够。有些情况需要程序和收集器之间进行更多的协作。例如,如果要将一个对象放在一个数组中,这看似很简单,好像只需要把每个对象插入数组 即可。但是,当一个对象处于数组中时,他就无法被回收。这是因为即使当前没有地方在使用它,但数组仍引用着它。除非用户告诉Lua这项引用不应该阻碍此对象的回收,否则Lua是无从得知这个事实的。
弱引用table就是这样一种机制,用户能用它来告诉Lua一个引用不应该阻碍一个对象的回收。所谓“弱引用”就是一种会被垃圾收集器忽视的对象引用。如果一个对象的所有引用都是弱引用,那么Lua就可以回收这个对象了,并且还可以以某种形式来删除这些弱引用本身。Lua用“弱引用table”来实现“弱引用”,一个弱引用table就是一个具有弱引用条目的table。如果一个对象只被一个弱引用table所持有,那么最终Lua是会回收这个对象的。
table中有key和value,这两个都可以包含任意类型的对象 。通常,垃圾收集器不会 回收一个可访问table中作为key或value的对象。也就是说,这些key和value都是强引用,它们会阻止对其所引用对象的回收。在一个弱引用table中,key和value是可以回收的。有3种弱引用table:具有弱引用key的table、具有弱引用value的table、同时具有两种弱引用table。不论哪种类型的弱引用table,只要有一个key或value被回收了,那么它们所在的整个条目都会从table中删除。
一个table的所引用类型是通过其元表中的__mode字段来决定的。这个字段的值应为一个字符串,如果这个字符串中包含字母‘k’,那么这个table的key是弱引用的;如果这个字符串中包含 字母‘v',那么这个table的value是弱引用的。下面这个示例虽然是认为制造的,但演示了弱引用table的一些基本行为:
a = {}
b = {__mode = "k"}
setmetatable(a, b) --现在a的key就是弱引用
key = {} --创建第一个key
a[key] = 1
key = {} --创建第二个key
a[key] = 2
collectgarbage() --强制进行一次垃圾收集
for k, v in pairs(a) do print(v) end -->2
在本例中,第二句赋值key = {}会覆盖第一个key。当收集器运行时,由于没有其他地方在引用第一个key,因此第一个key就被回收了,并且table中的相应条目也被删除了。至于第二个key,变量key仍引用着它,因此它没有被回收。
注意,Lua只会回收 弱引用table中的对象。而像数字和布尔这样的“值”是不可回收的。例如,对于一个插入table的数字key,收集器是永远不会删除它的。当然,如果一个数字key对应的value被回收了,那么整个条目都会从弱引用table中删除。
字符串在此则显得有些特殊。虽然从实现的角度看,字符串是可回收的。但在 有些方面, 字符串却与其他可回收的对象不同。其他对象,例如table和函数都是显示创建的。又如,当Lua对表达式{}求值时,它就会创建一个新的table。同样地,求值function()...end时 就会创建一个新函数。然而,当Lua对“a" .. "b"求值时,它会创建一个新字符串吗?如果当前系统中已有了一个字符串"ab",它会复用吗?还是创建一个新的字符串?编译器会在运行程序前先创建这个字符串吗?这些都无关紧要,它们都是实现的细节。从程序猿的角度看,字符串就是值,而非对象。因此字符串就像数字和布尔一样,不会从弱引用table中删除。

备忘录( memoize)函数
一项通用的编程技术是“用空间换时间”。例如有一种做法就可以提高一些函数的运行速度,记录下函数的计算结果, 然后当使用同样的参数再次调用该函数时,便可以复用之前的结果了。
假设有一个普通的服务器,在它收到的请求中包含Lua代码。每当服务器收到一个请求,它就要对代码字符串调用loadstring,然后再调用编译好的函数。不过loadstring是一个昂贵的函数,而有些发给服务器的函数具有很高的频率,例如“closeconnection()”。与其每次收到一条常见命令就调用loadstring,还不如让服务器用一个辅助table记录下所有调用 loadstring的结果。
local results = {}
function mem_loadstring (s)
local res = results[s]
if res == nil then
res = assert(loadstring(s))
results[s] = res
end
return res
end
这项优化节省的时间非常可观。但,它也会导致不易察觉的开销。虽然有些命令会重复出现,但还有许多命令只发生一次。table results会逐渐地累积服务器收到的所有命令及其编译结果。经过一段时间后,这种累积会消耗服务器的内存。弱引用table正好可以解决这个问题,如果results table具有弱引用的value,那么每次垃圾收集都会删除所有在执行时未使用的编译结果。
local results = {}
setmetatable(results, {__mode = "kv"}) --由于key总是字符串,则可以使这个table变成完全弱引用。
function mem_loadstring (s)
<如前>
“备忘录”技术还可以用于确保某类对象的唯一性。假设一个系统用table来表示颜色,其中3个字段red、green和blue都具有相同的取值范围。最简单的颜色生成函数是:
function createRGB (r, g, b)
return {red = r, green = g, blue = b}
end
通过备忘录技术,可以复用具有相同颜色的table。备忘录table的key可以根据颜色分量来生成,本例中是将颜色分量以分隔符连接起来:
local results = {}
setmetatable(results, {__mode = "v"})
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
这种实现可以使用户通过原始的相等性操作符比较两种颜色。若两种同时存在的颜色 相等,那么它们必定是由同一个table表示的。

对象属性
关于弱引用table,还有一项重要的引用是将对象和属性关联起来。有很多情况需要把有些属性绑定到某个对象,例如函数与其名称、table的默认值以及数组的大小等。
当对象是一个table时,可以通过适当的key将属性存储在这个table中。正如前面所看到的,创建唯一性key的最简单办法是创建一个新对象(通常是一个table)。不过,若对象不是一个table,它就无法保存属性了。另外,即使是table,有时也不想将属性存储在原table中。例如,想保持属性的私有性,或者不想属性扰乱table的遍历就需要用其他办法来关联属性与对象了。显然,使用外部table来关联它们是一种理想的做法。可以将对象作为key,对象的属性作为value。这个外部table可以保存任意对象的属性,Lua也允许将任何对象作为table的key。另外,存储在外部对象中的属性不会干扰其他对象,只要table本身是私有的,这些属性也会是私有的。
然而,这个看似完美的做法却有一个 重大缺陷。当用户将一个对象作为外部table的key时,就是引用了它。Lua是无法回收一个作为table key的对象。如果用这个外部table来关联函数和函数名,那么这些函数就永远无法回收。用户可以使用弱引用table来解决这个问题。而本例需要的是弱引用key。当一个弱引用key没有其他引用时,Lua就可以回收它。注意,这个table不能使用弱引用value,否则“存留的”对象的属性就有可能被回收。

回顾table的默认值
第一种方法:
function setDefault (t, d)
local mt = {__index = function return d end}
setmetatable(t, mt)
end

local key = {} --唯一的key
local mt = {__index = function (t) return t[key] end}
function setDefault (t, d)
t[key] = d
setmetatable(t, mt)
end
第二种方法:
使用一个弱引用table,通过它将每个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
如果 defaults没有弱引用key,它就会使所有具有 默认值的table持久存在下去。
第三种方法:
对每种不同的默认值使用不同的元表。不过,只要有重复的默认值,就 复用同样的元表。这是备忘录的典型应用:
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
end
setmetatable(t, mt)
end
这里用到了弱引用value,这样当metas中的元表在不使用时就可以被回收了。
这后两种默认值的实现,哪种更好呢?一般而言,它们具有类似的复杂度和性能表现。第二种做法是需要为每个table的默认值(defaults中的一个条目)使用内存。而第三种做法则需要为每种不同的默认值使用一组内存(一个新table、一个新closure和metas中的一个条目)。因此,如果程序中有上千个table和一些默认值,第三种做法则是首选。但如果只有很少的table共享几个 公用的默认值,那么就应该选择第二种做法。





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值