Lua弱表Weak table

    转载

    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、kv。k表示table.key是weak的,也就是table的keys能够被自动gc;v表示table.value是weak的,也就是table的values能被自动gc;kv就是二者的组合。任何情况下,只要key和value中的一个被gc,那么这个key-value pair就被从表中移除了

    对于普通的强引用表,当你把对象放进表中的时候,就产生了一个引用,那么即使其他地方没有对表中元素的任何引用,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值是不会因此而被当做垃圾的。
    换句话说,key1本身已经是nil值,但它曾经所指向的内容依然存放在t中。key2也是一样的情况。所以我们最后还是能输出key1和key2的name字段。

    如果我们把某个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的所有字段都不存在了。
    这就是弱引用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。我们可以将这个操作包装为一个函数:

    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表的简单应用——关联对象属性

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

    --[[在第一种解决方案中,我们使用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,那么你还是用第一种方法吧。

    你可能感兴趣的:(Lua,弱表)