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
,传递的参数是table
和key
,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
原文翻译:
控制弱引用,用字符k
和v
来代表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中所有的元方法就分析完了,还有一些操作符重载的,之后再写。