弱引用 table

  • lua 的垃圾回收器只会回收没有引用的对象,有些时候并不能回收程序员认为的垃圾。比如数组里的元素在其它地方已经没有引用了,但因为还在数组中,因此垃圾回收器并不会去回收它
  • 弱引用 table 告诉回收器一个元素在 table 中的引用不应该阻止它的回收。如果一个对象的引用都是弱引用,那回收器就会回收这个对象
  • 弱引用 table 有三种:弱引用 key,弱引用 value 和弱引用 key-value。设置 table 的元表的 __mode 元字段就可以设置一个表为弱引用表,__mode = “k” 表示是弱引用 key 表,__mode = “v” 表示是弱引用 value 表,__mode = “kv” 表示是弱引用 key-value 表
  • lua 只会回收弱引用 table 中的对象,对于数字、布尔、字符串此类的值是不会回收的
table = {}
setmetatable(table, {__mode = "kv"})

key = {}
table[key] = 10
key = {}
table[key] = 20
table.x = {}
table.y = "hello"

for k, v in pairs(table) do
    print(k, v)
end

print("----------")

-- 强制垃圾回收
collectgarbage()

for k, v in pairs(table) do
    print(k, v)
end

看一下输出结果

table: 005FC1A8 20
table: 005FC158 10
x   table: 005FC180
y   hello
----------
table: 005FC1A8 20
y   hello

原来的 table 共有 4 个数据,强制垃圾回收后只剩下两个。第二个 key 定义的时候会覆盖掉第一个,因此第一个 key 所指的对象就没有引用了;另外 x 和 y 的值也没有引用,但 y 的值是字符串,其 key-value 类型都是值类型,因此不会被回收

备忘录 memoize

备忘录是一种以空间换时间的机制,调用一个函数时,将某些参数的计算结果保存到一个备忘录 table 中,下次调用函数传进来同样的参数时,直接从备忘录 table 中取值。这种方式有个问题就是很容易产生垃圾数据,某些结果可能只使用一次,后面不会再用到了,但这些结果被备忘录 table 引用着,因此不会被回收。解决的方法就是使用弱引用 table。

local color = {}
setmetatable(color, {__mode = "v"})

create_color = function(r, g, b)
    local key = r .. "-" .. g .. "-" .. b
    local res = color[key]
    if res == nil then
        res = {red = r, green = g, blue = b}
        color[key] = res
    end
    return res
end

local color1 = create_color(255, 255, 255)
local color2 = create_color(255, 255, 255)
print(color1 == color2) -- true

对象属性

有时候需要将属性绑定到一个对象,如果是对象是 table 和话可以定义几个 key 来保存这些属性,但如果是其它对象或者不想打乱 table 的结构的话就必须另外找方法了。比如一个函数要绑定一个名字属性,一个数组要绑定一个大小属性,一个表要绑定一个默认值属性。有一种解决方案就是使用一个外部 table 来保存对象和属性之间的对应关系,但这样的话这些对象就被多引用了一次,可能导致永远无法回收,这时很自然地就想到使用弱引用 table。这里使用弱引用 key,即当对象没有引用时回收,而不是当属性没引用时回收

local property = {}
local mt = {__mode = "k"}
setmetatable(property, mt)

local array = {1, 2, 3, 4, 5}

-- 保存对象属性
property[array] = {size = 10}

for i = 1, property[array].size do
    print(i, array[i])
end

array = nil
collectgarbage()

print(#property) -- 0

回顾 table 的默认值

在介绍元表的时候我们已经讲过可以使用 table 的 __index 元方法来设置 table 的默认值,前面介绍了两种方式,这里再介绍两种

使用弱引用 table 保存每个 table 和它的默认值

local defaults = {}
setmetatable(defaults, {__mode = "k"})

local mt = {__index = function(t)
        return defaults[t]
    end}

set_default = function(t, d)
    defaults[t] = d
    setmetatable(t, mt)
end

local t = {}
set_default(t, 100)
print(t.x)

这种方式把表的默认值保存到一个外部 table 中,然后所有的 table 共享一个元表,这个元表的 __index 元方法从外部 table 中获取默认值。把外部 table 设计成弱引用 table,这样垃圾回收器才能正常回收 table 对象

把 table 的元表设计成备忘录

local metas = {}
setmetatable(metas, {__mode = "k"})

local set_default = function(t, d)
    local mt = metas[d]
    if mt == nil then
        mt = {__index = function()
                return d
            end}
        metas[d] = mt
    end
    setmetatable(t, mt)
end

local t = {}
set_default(t, 666)
local tt = {}
set_default(tt, 666)
print(t.x, tt.y)

这种方式会给每个 table 设计一个新的元表,然后把默认值-元表保存到外部 table 中,下次如果一个新的 table 的默认值在外部 table 中可以找到,则使用之前定义的元表而不新创建元表

总结

到目前为止设置 table 的默认值共有四种方式,共同的思想是使用元表的 __index 元方法来返回默认值,不同的点有是独立元表还是共享元表,默认值存放在哪里
1. 每个 table 独立一个 metatable,由 __index 元方法直接返回默认值
2. 所有 table 共享一个 metatable,默认值存放在每个 table 本身,__index 元方法从 table 中取默认值返回
3. 所有 table 共享一个 metatable,所有默认值存放在一个外部 table 中,__index 元方法从外部 table 中取默认值返回
4. 一个默认值对应一个 metatable,存放在外部 table 中,使用备忘录的方式给每个 table 设置元表
* 最简单的方式是第一种,但这种方式需要的时间和空间都比较大
* 第二种跟第三种很相似,都是共享一个元表,然后保存默认值;这种方式省去了频繁创建元表的时间,空间上也较第一种有改进
* 第四种虽然也是为每个表创建一个元表,但相同的默认值使用了备忘录的机制,空间和时间都比第一种要好

选择: 如果 table 很多且有很多重复的默认值,则使用第四种方式;如果只有个别的 table 或者默认值很少重复,则使用第三种方式