Metatable和Metamethod


  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是一種弱類型化語言,所以對於很多概念的具體行為你一定要自己多加測試,不能夠想當然。

 

 

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM