如灰尘保佑的礼物

深度理解Lua中的table

Lua的table是个很有意思的东西。有些内容平时写代码的时候很少接触到,但是了解一下还是很有意思的。

这篇blog参考MetatableEvents,一个一个边写测试边细说。

__newindex

原文翻译:

__newindex用于分配属性,当调用 myTable[key]=value时,如果元表中有__newindex并且指向一个function,就会调用这个function,传入的参数为table, key 和 value

  • rawset(myTable, key, value)可以跳过这个元方法直接给myTable的key属性赋值为value。
  • 如果__newindex指向的方法中,没有调用rawset方法,传入的键值对(key/value)就不会添加到myTable中。

测试代码:

local meta = {
    __newindex = function(t, key, value)
        print("call __newindex",t, key, value)
    end
}

local test = {}
setmetatable(test, meta)

print("test", test)
print("meta", meta)

test.name = "t1"
test.name = "t2"
print("test.name", test.name)

---- result output ----
test	table: 0x7f9c13406f00
meta	table: 0x7f9c13407240
call __newindex	table: 0x7f9c13406f00	name	t1
call __newindex	table: 0x7f9c13406f00	name	t2
test.name	nil

测试代码中,当给t的name的赋值时,就会触发元表中的__newindex指向的function,打印的信息可以看到key和value的值。

__newindex方法中传进来的参数t的指针和test的指针指向同一个地址,说明__newindex中的参数t,并不是元表。

测试代码中对t.name连续赋值时,__newindex会连续调用,需要留意一下这里,后面的测试会跟这里做一个对比。

赋值之后打印 t.name 的值是空的。原因是__newindex并没有给t.name赋值,我们用一个错误的方式给t.name赋值,来加深__newindex的理解。修改一下测试代码:

local meta = {
    __newindex = function(t, key, value)
        print("call __newindex",t, key, value)
        t[key] = value
    end
}

local test = {}
setmetatable(test, meta)

print("test", test)
print("meta", meta)

test.name = "t1"
test.name = "t2"
print(test.name)

---- result output ----
...
lua: C stack overflow
...

报错信息,栈溢出。因为t[key] = value这段代码会调用t元表中的__newindex的方法,__newindex的方法又会调用t[key] = value,这样就进入了死循环,导致栈溢出。这时就需要用到方法rawset

修改测试代码:

local meta = {
    __newindex = function(t, key, value)
        print("call __newindex",t, key, value)
        rawset(t, key, value)
    end
}

local test = {}
setmetatable(test, meta)

test.name = "t1"
test.name = "t2"
print("test.name", test.name)

---- result output ----
call __newindex	table: 0x7fdade404e20	name	t1
test.name	t2

这段代码中信息比较多

__newindex中使用了rawset方法,可以看到,没有栈溢出的错误了,说明用rawset给table赋值,不会进入__newindex 的方法。

给t.name连续赋值,会发现只进入__newindex一次,跟之前不同的是,我们在__newindex给t.name赋了值。如果t中没有这个key时,才会进入__newindex方法。否则不会进入。

__newindex的默认值就是上面meta.__newindex的代码。如果不需要额外处理,完全可以不写。如下:

local meta = {}
local test = {}
setmetatable(test, meta)

test.name = "t1"
print("test.name", test.name)

---- result output ----
test.name	t1

__index

翻译原文

__index用于控制属性(prototype)的继承,当访问 myTable[key] 时,如果myTable中不存在这个key,但是如果元表(metatable)中有 __index时:

  • 如果__index是一个function,传递的参数是tablekey,function的返回值作为结果返回。
  • 如果__index是一个table,就返回这个表中key对应的值。
    • 如果这个table不存在该key,但是这个table有元表,会继续寻找元表中的__index属性,以此类推。都没有就返回nil
  • 使用 “rawget(myTable,key)” 可以跳过这个元方法(__index).

写点测试:

local test = {}

local meta = {
    __index = function(t, k)
        print("__index", k)
        if rawget(t, k) == nil then
            print("Can't find ".. k)
        end

        return rawget(t, k)
    end,
}

setmetatable(test, meta)

print("test.name1", test.name)
test.name = "hello"
print("test.name2", test.name)


---- result output ----

__index	name
Can't find name
test.name1	nil
test.name2	hello

__newindex__index其实可以类比成setter和getter,这么类比会比较容易理解,但是实际上还是有比较大的区别。

上面的测试中__index是个function。当执行test.name时,如果test.name是nil,会调用__index的function,并返回function的返回值。否则,直接返回test[key],不会进入__index

再做一个测试,这次__index是个table

local test = {}

local meta = {
    __index = {name="meta"},
}

setmetatable(test, meta)

print("test.name1", test.name)
test.name = "hello"
print("test.name2", test.name)

---- result output ----

test.name1	meta
test.name2	hello

这个测试可以看到,访问顺序是先访问test的name,如果没有值,再访问test元表中__index的table。如果test的元表还有元表,会继续向上访问,Lua继承的实现就是利用这个特性。

掌握__newindex__index这两个元方法,可以把这两个元方法看做两个事件,那要就要清楚两个方法的触发条件和特性。才能融会贯通。

举个例子:

禁用全局变量

local meta = {
    __newindex = function(t, k, v)
        print("Error! Can't set globle variable", k)
    end,

    -- 默认实现
    -- __index = function(t, k)
    --     return rawget(t, k)
    -- end
}

setmetatable(_G, meta)

test = "test"
print(test)

---- result output ----

Error! Can't set globle variable	test
nil

__mode

原文翻译:

控制弱引用,用字符kv来代表table的是否是弱引用。这个感觉没什么好说的,只写个测试就好了。

local meta = {__mode = "k"}
local test = {}
setmetatable(test, meta)
key = {}
test[key] = 1
key = {}
test[key] = 2
for k,v in pairs(test) do
    print(v)
end

collectgarbage()
print("collectgarbage")

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

---- result output ----

1
2
collectgarbage
2

例子中当调用collectgarbage()进行回收后,test表中只剩下一个值。弱引用的key被清理了。我们也可以在__mode中设置v,kv来表示 键和值都是弱引用。

__call

原文翻译:

把table当做一个function使用,当table后跟一个圆括号时,而且table的元表中的__call指向一个function,就会调用这个function,table自己做为第一个参数,后面可接任意数量的参数,返回值就是function的返回值。

测试代码来模拟实现一个构造方法。

local meta = {
    __call = function(t, ...)

        local instance = {}
        for k, v in pairs(t) do
            instance[k] = v
        end
        return instance
    end
}

local A = setmetatable({}, meta)

function A:info()
    print("info",self)
end

local a = A()
local b = A()

a:info()
b:info()

---- result output ----
info	table: 0x7f8771e05030
info	table: 0x7f8771e050a0

__metatable

原文翻译:

隐藏真正的元表,当调用getmetatable时,而且table的元表有__metatable字段,则返回__metatable字段中的值。

测试代码:

local meta = {
    name = "meta"
}

local test = setmetatable({}, meta)
print(getmetatable(test).name)

local meta = {
    __metatable = {name = "__metatable"},
    name = "meta"
}

local test = setmetatable({}, meta)
print(getmetatable(test).name)

---- result output ----
meta
__metatable

结果很直观不解释了,我另外还做了个的测试,让__metatable指向了一个function,调用getmetatable时也会返回这个function。很有意思,但是暂时没想到有什么应用场景。

__tostring

原文翻译:

控制字符串的表现,当调用tostring(myTable)时,且myTable的元表中有__tostring字段时,就会调用这个方法。返回值是方法的返回值。

测试代码:

local meta = {
    __tostring = function(t)
        return string.format("My name is %s", t.name)
    end
}

local test = setmetatable({}, meta)
test.name = "test"
print(test)
print(tostring(test))

---- result output ----
My name is test
My name is test

这个也不做过多说明了,很容易理解。有一点提一下就是print方法会自动调用tostring(test)

__len

原文翻译:

控制table的长度。当用#操作符请求长度时,且table的元表有__len字段指向一个function,就会调用这个function,参数是table自己,返回值是function的返回值。

写个测试代码:

local meta = {
    __len = function(t)
        local result = 0
        for k, v in pairs(t) do
            result = result + 1
        end
        return result
    end
}


local test = {
    [1] = "A",
    [2] = "B",
    [3] = "C",
    [5] = "D",
    [6] = "E",
    [8] = "F",
}
print(#test)

setmetatable(test, meta)
print(#test)

---- result output ----
3
6

Lua中使用#获取长度有个特性,就是如果某个key对应的值是nil就结束,上面例子中,test第4个值是nil,那返回的长度为3。我们重新定义了__len后返回了,用遍历的方式计算长度,返回table内元素的数量为6。

__gc

原文翻译:

简单说就是数据被垃圾回收的时候会首先触发__gc

测试代码:

local meta = {

    __gc = function(t)
        print("gc")
    end
}

local function test()
    local test = {}
    setmetatable(test, meta)
end

test()

---- result output ----
gc

Lua table中所有的元方法就分析完了,还有一些操作符重载的,之后再写。