Metatable和Metamethod是用來干啥的?它們可以使得表a和b的表達式“a + b”變得有意義,其中metatable使兩個不相關的表a和b之間可以進行操作,而操作的具體行為比如說"+"由metamethod來具體定義。
Metatable和Metamethod大多數地方都翻譯成“元表”和“元函數”,這是一種直譯,相當不直觀。根據Metatable的用法,我傾向於將Metatable翻譯成關聯表,Metamethod翻譯成關聯函數。通過給兩個table設置Metatable可以使兩個table產生聯系,然后對兩個table進行一些操作,具體的操作行為由Metamethod來定義。下面的例子中,在對表t1和t2設置關聯表mt,並在mt中定義關聯函數__add后,就可以對這兩個表進行"+"相加操作了。
t1 = {1, 2, 3} t2 = {4,5,6,7,8} mt = {} mt.__add = function(a, b) local ret = 0 for _, v in pairs(a) do ret = ret + v end for _, v in pairs(b) do ret = ret + v end return ret end setmetatable(t1, mt) setmetatable(t2, mt) print(t1 + t2)
從上面的代碼中可以看到關聯表就是一個表,而關聯函數就是一個函數。當碰到表達式"t1+t2"時,Lua首先查找他們的關聯表,找到關聯表mt后,會在mt中找與相加操作對應的關聯函數__add,找到__add后就將t1和t2作為參數來執行該函數,最后返回結果。
下面是一個使用關聯表來對集合(用table實現的集合)進行操作的示例,實例中定義了集合的並集、交集、比較等運行:
Set = {} --專門用來作為metatable,定義在Set里面以免影響外部的命名空間 Set.mt = {} --轉化為string Set.tostring = function (set) local s = "{" local sep = " " for e in pairs(set) do s = s .. sep .. e sep = ", " end return s.."}" end --打印 Set.print = function(s) print(Set.tostring(s)) end Set.mt.__tostring = Set.tostring --新建一個集合 Set.new = function (t) local set = {} setmetatable(set, Set.mt) --指定所創建集合的metatable for _, l in ipairs(t) do set[l] = true end return set end --並集 Set.union = function (a,b) local res = Set.new{} for k in pairs(a) do res[k] = true end for k in pairs(b) do res[k] = true end return res end --給metatable增加__add函數(metamethod),當Lua試圖對兩個集合相加時,將調用這個函數,以兩個相加的表作為參數 Set.mt.__add = Set.union --交集 Set.intersection = function (a,b) local res = Set.new{} for k in pairs(a) do res[k] = b[k] end return res end --定義集合相乘操作為求交集 Set.mt.__mul = Set.intersection --先定義"<="操作,然后基於此定義"<"和"=" Set.mt.__le = function (a, b) for k in pairs(a) do if not b[k] then return false end end return true end --小於 Set.mt.__lt = function(a, b) return a<=b and not (b <= a) end --等於 Set.mt.__eq = function(a, b) return a <= b and b <= a end --測試 s1 = Set.new{1, 2, 3} s2 = Set.new{10, 20, 30, 40, 50} print(getmetatable(s1)) print(getmetatable(s2)) s3 = s1 + s2 --等同於Set.union(s1, s2) print(s3) print(s3 * s2) print(s1 <= s3) print(s1 == s3) print(s1 < s3) print(s1 >= s3) print(s1 > s3) --起保護作用,getmetatable將返回這個域的值,而setmettable將會出錯 Set.mt.__metatable = "not your business" print(getmetatable(s1)) setmetatable(s1, {})
當Lua試圖對兩個表進行相加時,他會檢查兩個表是否有一個表有Metatable,並且檢查Metatable是否有__add域。如果找到則調用這個__add函數(所謂的Metamethod)去計算結果。當兩個表有不同的Metatable時,以誰的為准呢?Lua選擇metamethod的原則:
(1)如果第一個參數存在帶有__add域的metatable,Lua使用它作為metamethod,和第二個參數無關;
(2)否則,第二個參數存在帶有__add域的metatable,Lua使用它作為metamethod;
(3)否則,報錯。
Lua中定義的常用的Metamethod如下所示:
算術運算符的Metamethod:__add(加運算)、__mul(乘)、__sub(減)、__div(除)、__unm(負)、__pow(冪),__concat(定義連接行為)。
關系運算符的Metamethod:__eq(等於)、__lt(小於)、__le(小於等於),其他的關系運算自動轉換為這三個基本的運算。
庫定義的Metamethod:__tostring(tostring函數的行為)、__metatable(對表getmetatable和setmetatable的行為)。
注意:__metatable不是函數,而是一個變量。假定你想保護你的集合使其使用者既看不到也不能修改metatables。如果你對metatable設置了__metatable的值,getmetatable將返回這個域的值,而調用用setmetatable將會出錯:
注意:相等比較從來不會拋出錯誤,如果兩個對象有不同的metamethod,比較的結果為false,甚至可能不會調用metamethod。這也是模仿了Lua的公共的行為,因為Lua總是認為字符串和數字是不等的,而不去判斷它們的值。僅當兩個有共同的metamethod的對象進行相等比較的時候,Lua才會調用對應的metamethod。
print總是調用tostring來格式化它的輸出,tostring會首先檢查對象是否存在一個帶有__tostring域的metatable。
表相關的Metamethod:
(1)__index metamethod:在繼承中使用較多。當訪問表不存在的一個域時,會觸發Lua解釋器去查找__index metamethod,如果不存在,則返回nil,否則由__index metamethod返回結果。
Window = {x = 0, y = 0, width = 100, height = 100} mt = {} mt.__index = function(table, key) return Window[key] end w = {x = 10, y = 20} setmetatable(w, mt) print(w.width)
可以看到w沒有width域,但有關聯表mt,且關聯表有__index,因此w.width會觸發mt.__index的調用(Lua會將w作為第一個參數、width作為第二個參數來調用該函數)。
__index除了作為一個函數,還可以直接作為一個表來使用當__index是一個表時,Lua會直接在這個表中查找width域。因此代碼也可以像這樣來寫:
Window = {x = 0, y = 0, width = 100, height = 100} mt = {} mt.__index = Window w = {x = 10, y = 20} setmetatable(w, mt) print(w.width)
rawget(table, index)函數獲取表中指定域的值,該函數可以繞過metamethod,直接返回表的域信息,看下面這個例子:
Window = {x = 0, y = 0, width = 100, height = 100} mt = {} mt.__index = function(table, key) return Window[key] end w = {x = 10, y = 20} setmetatable(w, mt) print(w.width) --100 print(rawget(w, "width")) --nil print(rawget(w, "x")) --10
看上面倒數第二行,rawget(w, "width")訪問不存在的域不會觸發查找__index。
(2)__newindex metamethod:__newindex metamethod用來對表更新,__index則用來對表訪問。
當給表的一個不存在的域賦值時(比如w.add = 1),會觸發Lua查找__newindex,如果不存在__newindex,則像一般的賦值行為一樣導致表添加了一個域。
(1)不存在__newindex,則像一般的賦值行為一樣導致表添加了一個域(w多了一個域add,值為1)
(2)存在__newindex,則不進行賦值操作,而是由__newindex攔截了賦值操作,並且將(table、域名、值)作為參數調用__newindex。
也就是說,__newindex可以使得任何對表的添加元素的行為都要經過__newindex,這確實是一個很好的把關。
rawset(t, k, v)函數也是一個等同於賦值的操作(w.add = 1相當於rawset(w, "add", 1)),但調用該函數可以繞過metamethod,即不會導致__newindex的調用:
mt = {} mt.__newindex = function(table, key, value) rawset(table, key, value) --這里不能寫成table.key = value;因為這個給不存在域的賦值操作又會導致__newindex的調用,因而陷入死循環 end w = {x = 10, y = 20} setmetatable(w, mt) w.add = 1 print(w.add) -- 1
和__index一樣,__newindex也可以是一個表,如果__newindex是一個表,會導致對指定的那個表而不是原始的表進行賦值操作。
Window = {x = 0, y = 0, width = 100, height = 100} mt = {} mt.__newindex = Window w = {x = 10, y = 20} setmetatable(w, mt) w.add = 1 print(w.add) --nil print(Window.add) --1
賦值操作導致Windows添加了一個元素add,而w不影響。
當__index和__newindex混合使用時,一定要注意區分每個行為都干了什么事情:
Window = {x = 0, y = 0, width = 100, height = 100} mt = {} mt.__index = Window mt.__newindex = Window w = {x = 10, y = 20} setmetatable(w, mt) w.add = 1 print(w.add) --1 print(Window.add) --1
__newindex為表時,w.add=1表示給Window添加了add,但通過__index,w也能訪問到Window的add。
再看下面這個例子:
Window = {x = 0, y = 0, width = 100, height = 100} mt = {} mt.__index = function(table, key) return Window[key] end mt.__newindex = function(table, key, value) rawset(table, key, value) end w = {x = 10, y = 20} setmetatable(w, mt) w.add = 1 print(w.add) -- 1 print(Window.add) -- nil
__newindex為函數時,w.add直接給w添加了add域,但Window並不存在add。所以結論是:當__newindex是函數時,給目標表w添加域;當__newindex是表時,給指向表添加域。
關於__index和__newindex一定要注意區分,什么時候進入__index,什么時候進入__newindex:
Window = {x = 0, y = 0, width = 100, height = 100} mt = {} mt.__index = function(table, key) print("going here __index") return Window[key] end w = {x = 10, y = 20} setmetatable(w, mt) s = w.width -- going here __index 訪問語句會進入__index w.width = 1 -- 賦值語句不會進入__index,這里會導致w表添加width域 print(w.width) -- 1 print(rawget(w, "width")) -- 1 print(Window.width) -- 100
訪問語句進入__index,賦值語句不進入__index。
mt = {} mt.__newindex = function(table, key, value) print("going here __newindex") rawset(table, key, value) end w = {x = 10, y = 20} setmetatable(w, mt) s = w.add -- 訪問語句不會進入__newindex w.add = 1 -- going here __newindex 賦值語句進入__newindex print(w.add) -- 1 print(rawget(w, "add")) -- 1
賦值語句進入_newindex,訪問語句不進入__newindex。
結合上面這兩句話就很容易理解下面這個例子了:
Window = {x = 0, y = 0, width = 100, height = 100} mt = {} mt.__index = function(table, key) print("going here __index") return Window[key] end mt.__newindex = function(table, key, value) print("going here __newindex") rawset(table, key, value) end w = {x = 10, y = 20} setmetatable(w, mt) s = w.width -- going here __index w.width = 1 -- going here __newindex print(w.width) -- 1 print(rawget(w, "width")) -- 1
倒數第三條語句w.width = 1不會進入__index,所以會導致給w表添加新的域width。也就是說__index邏輯不會影響__newindex的判斷,雖然__index可以訪問到域width,但__newindex依然仍未w沒有width域。
這些概念非常的繞,而且Lua是一種弱類型化語言,所以對於很多概念的具體行為你一定要自己多加測試,不能夠想當然。