Programming in Lua, 2Nd Edition - Chapter 17: Weak Tables

 

 

 

 

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 tablekey weak,或value weak,或keyvalue 全是weak和表的类型无关,当一个key 被收集或是一个value  被收集,整个entry 都会从weak table中消失。

 


 

这样来决定一个weak tablekeyvalue是否是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 中收集。值类型,如数字和truefalse 这种布尔值不被收集。例如,我们在weaka 中插入数字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"}) ,结果是一样的。

 


 

缓存技术也用来确保一些类型对象的唯一。例如,假设系统将颜色表示为表,有redgreen,和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

 

这个例子中,不同的默认值有不同的元表。

 

这两种实现对象默认值的方法哪一种比较好?实际上,要视情况而定。这两种方法有着相似的复杂性和相似的性能。第一种实现需要为表的默认值消耗许多内存。第二种实现为不同的默认值消耗大把内存。所以,如果你的程序有非常多带有不同默认值的表,第二种方案具有出众的性能。另一方面,如果许多表共享相同的默认值,你就应该用第一种方案。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值