Lua弱表Weak table

弱表(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行为 

weak table的定义--弱引用

弱表的使用就是使用弱引用,很多程度上是对内存的控制。

  • weak表是一个表,它拥有metatable,并且metatable定义了__mode字段
  • weak表中的引用是弱引用(weakreference),弱引用不会导致对象的引用计数变化换言之,如果一个对象只有弱引用指向它,那么gc会自动回收该对象的内存。
  • __mode字段可以取以下三个值:k、v、kvk表示table.key是weak的,也就是table的keys能够被自动gc;v表示table.value是weak的,也就是table的values能被自动gc;kv就是二者的组合。任何情况下,只要key和value中的一个被gc,那么这个key-value pair就被从表中移除了

对于普通的强引用表,当你把对象放进表中的时候,就产生了一个引用,那么即使其他地方没有对表中元素的任何引用,gc也不会被回收这些对象。那么你的选择只有两种:手动释放表元素或者让它们常驻内存。

[javascript]  view plain  copy
  1. strongTable = {}  
  2. strongTable[1] = function() print("i am the first element") end  
  3. strongTable[2] = function() print("i am the second element") end  
  4. strongTable[3] = {10, 20, 30}  
  5.   
  6. print(table.getn(strongTable))  -- 3  
  7. collectgarbage()                          
  8. print(table.getn(strongTable))  -- 3  
在编程环境中,有时你并不确定手动给一个键值赋nil的时机,而是需要等所有使用者用完以后进行释放,在释放以前,是可以访问这个键值对的。这种时候,weak表就派上用场了

[javascript]  view plain  copy
  1. weakTable = {}  
  2. weakTable[1] = function() print("i am the first element") end  
  3. weakTable[2] = function() print("i am the second element") end  
  4. weakTable[3] = {10, 20, 30}  
  5.   
  6. setmetatable(weakTable, {__mode = "v"}) -- 设置为弱表  
  7.   
  8. print(table.getn(weakTable))      -->3  
  9.   
  10. ele = weakTable[1]                -- 给第一个元素增加一个引用  
  11. collectgarbage()  
  12. print(table.getn(weakTable))      -->1,第一个函数引用为1,不能gc  
  13.   
  14. ele = nil                         -- 释放引用  
  15. collectgarbage()  
  16. print(table.getn(weakTable))      -->0,没有其他引用了,全部gc  

当然在实际的代码过程中,我们不一定需要手动collectgarbage,因为该函数是在后台自动运行的,它有自己的运行周期和规律,对编程者来说是透明的。另一例子:

[javascript]  view plain  copy
  1. a = {}  
  2. b = {}  
  3. setmetatable(a,b)  
  4. b.__mode = "k"  --now 'a' has weak keys  
  5.   
  6. key = {}   --create first key  
  7. a[key] = 1  
  8.   
  9. key = {}   --create second key   
  10. a[key] = 2  
  11.   
  12. for k,v in pairs(a) do  
  13.     print(v) --1   2  
  14. end  
  15. collectgarbage()  --forces a garbage collection cycle  
  16. for k,v in pairs(a) do  
  17.     print(v) --2    
  18.     --[[第二个赋值语句key={}覆盖了第一个key的值。当垃圾收集器工作时,  
  19.     在其他地方没有指向第一个key的引用,所以它被收集了,因此相对应的table中的入口也同时被移除了。  
  20.     可是,第二个key,仍然是占用活动的变量key,所以它不会被收集。--]]      
  21. end  

要注意,只有对象才可以从一个weak table中被收集。比如数字和布尔值类型的值,都是不会被收集的

关于字符串的一些细微差别:从上面的实现来看,尽管字符串是可以被收集的,他们仍然跟其他可收集对象有所区别。其他对象,比如tables和函数,他们都是显示的被创建。比如,不管什么时候当Lua遇到{}时,它建立了一个新的table任何时候这个 function()。。。end建立了一个新的函数(实际上是一个闭包)。然而,Lua见到“a”..“b”的时候会创建一个新的字符串?如果系统中已经有一个字符串“ab”的话怎么办?Lua会重新建立一个新的?编译器可以在程序运行之前创建字符串么?这无关紧要:这些是实现的细节。因此,从程序员的角度来看,字符串是值而不是对象。所以,就像数值或布尔值,一个字符串不会从weak tables中被移除(除非它所关联的vaule被收集)。

弱应用实例

[cpp]  view plain  copy
  1. t = {};      
  2. -- 使用一个table作为t的key值  
  3. key1 = {name = "key1"};  
  4. t[key1] = 1;  
  5. key1 = nil;  
  6.       
  7. -- 又使用一个table作为t的key值  
  8. key2 = {name = "key2"};  
  9. t[key2] = 1;  
  10. key2 = nil;  
  11.      
  12. -- 强制进行一次垃圾收集  
  13. collectgarbage();  
  14.       
  15. for key, value in pairs(t) do  
  16.     print(key.name .. ":" .. value);  
  17. end  
  18. --输出  
  19. --key1:1  
  20. --key2:1  
虽然我们在给t赋值之后,key1和key2都赋值为nil了。但是,已经添加到table中的key值是不会因此而被当做垃圾的。
换句话说,key1本身已经是nil值,但它曾经所指向的内容依然存放在t中。key2也是一样的情况。所以我们最后还是能输出key1和key2的name字段。

如果我们把某个table作为另一个table的key值后,希望当table设为nil值时,另一个table的那一条字段也被删除。应该如何实现?
这时候就要用到弱引用table了,弱引用table的实现也是利用了元表。
我们来看看下面的代码,和之前几乎一样,只是加了一句代码:

[cpp]  view plain  copy
  1. t = {};      
  2. -- 给t设置一个元表,增加__mode元方法,赋值为“k”  
  3. setmetatable(t, {__mode = "k"});  
  4.       
  5. -- 使用一个table作为t的key值  
  6. key1 = {name = "key1"};  
  7. t[key1] = 1;  
  8. key1 = nil;  
  9.       
  10. -- 又使用一个table作为t的key值  
  11. key2 = {name = "key2"};  
  12. t[key2] = 1;  
  13. key2 = nil;  
  14.       
  15. -- 强制进行一次垃圾收集  
  16. collectgarbage();  
  17.       
  18. for key, value in pairs(t) do  
  19.     print(key.name .. ":" .. value);  
  20. end  
  21. --输出 为空  
留意,在t被创建后,立刻给它设置了元表,元表里有一个__mode字段,赋值为”k”字符串。如果这个时候大家运行代码,会发现什么都没有输出,因为,t的所有字段都不存在了。 
这就是弱引用table的其中一种,给table添加__mode元方法,如果这个元方法的值包含了字符串”k”,就代表这个table的key都是弱引用的。
一旦其他地方对于key值的引用取消了(设置为nil),那么,这个table里的这个字段也会被删除。
通俗地说,因为t的key被设置为弱引用,所以,执行t[key1] = 1后,t中确实存在这个字段。随后,又执行了key1 = nil,此时,除了t本身以外,就没有任何地方对key1保持引用,所以t的key1字段也会被删除。

weak表的简单应用——记忆函数

一个相当普遍的编程技术是用空间来换取时间。你可以通过记忆函数结果来进行优化,当你用同样的参数再次调用函数时,它可以自动返回记忆的结果。

想像一下一个通用的服务器,接收包含Lua代码的字符串请求。每当它收到一个请求,它调用loadstring加载字符串,然后调用函数进行处理。然而,loadstring是一个“巨大”的函数,一些命令在服务器中会频繁地使用。不需要反复调用loadstring和后面接着的closeconnection(),服务器可以通过使用一个辅助table来记忆loadstring的结果。在调用loadstring之前,服务器会在这个table中寻找这个字符串是否已经有了翻译好的结果。如果没有找到,那么(而且只是这个情况)服务器会调用loadstring并把这次的结果存入辅助table。我们可以将这个操作包装为一个函数:

[cpp]  view plain  copy
  1. local result = {}  
  2. function mem_loadstring(s)  
  3.     if result[s] then  
  4.         return result[s]  
  5.     else  
  6.         local res = loadstring(s)  
  7.         result[s] = res  
  8.         return res  
  9.     end  
  10. end  

这个方案的存储消耗可能是巨大的。尽管如此,它仍然可能会导致意料之外的数据冗余。尽管一些命令一遍遍的重复执行,但有些命令可能只运行一次。渐渐地,这个table积累了服务器所有命令被调用处理后的结果;早晚有一天,它会挤爆服务器的内存。一个weak table提供了对于这个问题的简单解决方案。如果这个结果表中有weak值,每次的垃圾收集循环都会移除当前时间内所有未被使用的结果(通常是差不多全部):

setmetatable(results, {__mode =\"v\"})   -- make values weak

事实上,因为table的索引下标经常是字符串式的,如果愿意,我们可以将table全部置weak:

setmetatable(results, {__mode =\"kv\"}) 

记忆技术在保持一些类型对象的唯一性上同样有用.例如,假如一个系统将通过tables表达颜色,通过有一定组合方式的红色,绿色,蓝色。一个自然颜色调色器通过每一次新的请求产生新的颜色:

[cpp]  view plain  copy
  1. function createRGB (r, g, b)  
  2.     return {red = r, green = g, blue = b}  
  3. end  

使用记忆技术,我们可以将同样的颜色结果存储在同一个table中。为了建立每一种颜色唯一的key,我们简单的使用一个分隔符连接颜色索引下标

[cpp]  view plain  copy
  1. local res = {}  
  2. setmetatable(res,{__mode = "v"})  
  3. function createRGB(r, g, b)  
  4.     local key = r .. "-" .. g .. "-" .. b  
  5.     if res[key] then  
  6.         return res[key]  
  7.     else  
  8.         local newcolor = {red = r,green = g,blue = b}  
  9.         res[key] = newcolor  
  10.         return newcolor  
  11.     end  
  12. end  

一个有趣的后果就是,用户可以使用这个原始的等号运算符比对操作来辨别颜色,因为两个同时存在的颜色通过同一个的table来表达。要注意,同样的颜色可能在不同的时间通过不同的tales来表达,因为垃圾收集器一次次的在清理结果table。然而,只要给定的颜色正在被使用,它就不会从结果中被移除。所以,任何时候一个颜色在同其他颜色进行比较的时候存活的够久,它的结果镜像也同样存活。

weak表的简单应用——关联对象属性

weak表的简单应用——带有默认值得表

[cpp]  view plain  copy
  1. --[[在第一种解决方案中,我们使用weak table来将默认vaules和每一个table相联系:  
  2. 使用weak table来将默认vaules和每一个table相联系--]]  
  3. local defaults = {}  
  4. setmetatable(defaults,{__mode = "k"})  
  5. local mt = {__index = function(t) return defaults[t] end}  
  6. function setDefault(t,d)  
  7.     defaults[t] = d  
  8.     setmetatable(t,mt)  
  9. end  
[cpp]  view plain  copy
  1. --[[如果默认值没有weak的keys,它就会将所有的带有默认值的tables设定为永久存在。在第二种方法中,  
  2. 我们使用不同的metatables来保存不同的默认值,但当我们重复使用一个默认值的时候,重用同一个相同  
  3. 的metatable。这是一个典型的记忆技术的应用:--]]  
  4. local metas = {}  
  5. setmetatable(metas,{__mode = "v"})  
  6.   
  7. function setDefault(t,d)  
  8.     local mt = metas[d]  
  9.     if mt == nil then  
  10.         mt = {__index = function() return d end}  
  11.         metas[d] = mt  --memoize  
  12.     end  
  13.     setmetatable(t,mt)  
  14. end  

这种情况下,我们使用weak vaules,允许将不会被使用的metatables可以被回收。

把这两种方法放在一起,哪个更好?通常,取决于具体情况。它们都有相似的复杂性和相似的性能。第一种方法需要在每个默认值的tables中添加一些文字(一个默认的入口)第二种方法需要在每个不同的默认值加入一些文字(一个新的表,一个新的闭包,metas中新增入口)。所以,如果你的程序有数千个tables,而这些表只有很少数带有不同默认值的,第二种方法显然更优秀。另一方面,如果只有很少的tabels可以共享相同的默认vaules,那么你还是用第一种方法吧。

猜你喜欢

转载自blog.csdn.net/qq_16209077/article/details/78552875
今日推荐