Lua的table是個很有意思的東西。有些內(nèi)容平時寫代碼的時候很少接觸到,但是了解一下還是很有意思的。
這篇blog參考MetatableEvents,一個一個邊寫測試邊細說。
__newindex
原文翻譯:
__newindex用于分配屬性,當調(diào)用 myTable[key]=value時,如果元表中有__newindex并且指向一個function,就會調(diào)用這個function,傳入的參數(shù)為table, key 和 value
- 用
rawset(myTable, key, value)可以跳過這個元方法直接給myTable的key屬性賦值為value。 - 如果
__newindex指向的方法中,沒有調(diào)用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的賦值時,就會觸發(fā)元表中的__newindex指向的function,打印的信息可以看到key和value的值。
__newindex方法中傳進來的參數(shù)t的指針和test的指針指向同一個地址,說明__newindex中的參數(shù)t,并不是元表。
測試代碼中對t.name連續(xù)賦值時,__newindex會連續(xù)調(diào)用,需要留意一下這里,后面的測試會跟這里做一個對比。
賦值之后打印 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這段代碼會調(diào)用t元表中的__newindex的方法,__newindex的方法又會調(diào)用t[key] = value,這樣就進入了死循環(huán),導致棧溢出。這時就需要用到方法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連續(xù)賦值,會發(fā)現(xiàn)只進入__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,傳遞的參數(shù)是table和key,function的返回值作為結(jié)果返回。 - 如果
__index是一個table,就返回這個表中key對應的值。- 如果這個
table不存在該key,但是這個table有元表,會繼續(xù)尋找元表中的__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,這么類比會比較容易理解,但是實際上還是有比較大的區(qū)別。
上面的測試中__index是個function。當執(zhí)行test.name時,如果test.name是nil,會調(diào)用__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的元表還有元表,會繼續(xù)向上訪問,Lua繼承的實現(xiàn)就是利用這個特性。
掌握__newindex和__index這兩個元方法,可以把這兩個元方法看做兩個事件,那要就要清楚兩個方法的觸發(fā)條件和特性。才能融會貫通。
舉個例子:
禁用全局變量
local meta = {
__newindex = function(t, k, v)
print("Error! Can't set globle variable", k)
end,
-- 默認實現(xiàn)
-- __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
例子中當調(diào)用collectgarbage()進行回收后,test表中只剩下一個值。弱引用的key被清理了。我們也可以在__mode中設置v,kv來表示值 鍵和值都是弱引用。
__call
原文翻譯:
把table當做一個function使用,當table后跟一個圓括號時,而且table的元表中的__call指向一個function,就會調(diào)用這個function,table自己做為第一個參數(shù),后面可接任意數(shù)量的參數(shù),返回值就是function的返回值。
測試代碼來模擬實現(xiàn)一個構造方法。
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
原文翻譯:
隱藏真正的元表,當調(diào)用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
結(jié)果很直觀不解釋了,我另外還做了個的測試,讓__metatable指向了一個function,調(diào)用getmetatable時也會返回這個function。很有意思,但是暫時沒想到有什么應用場景。
__tostring
原文翻譯:
控制字符串的表現(xiàn),當調(diào)用tostring(myTable)時,且myTable的元表中有__tostring字段時,就會調(diào)用這個方法。返回值是方法的返回值。
測試代碼:
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方法會自動調(diào)用tostring(test)
__len
原文翻譯:
控制table的長度。當用#操作符請求長度時,且table的元表有__len字段指向一個function,就會調(diào)用這個function,參數(shù)是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就結(jié)束,上面例子中,test第4個值是nil,那返回的長度為3。我們重新定義了__len后返回了,用遍歷的方式計算長度,返回table內(nèi)元素的數(shù)量為6。
__gc
原文翻譯:
簡單說就是數(shù)據(jù)被垃圾回收的時候會首先觸發(fā)__gc。
測試代碼:
local meta = {
__gc = function(t)
print("gc")
end
}
local function test()
local test = {}
setmetatable(test, meta)
end
test()
---- result output ----
gc
Lua table中所有的元方法就分析完了,還有一些操作符重載的,不細說了。