一文讀懂Lua元表

陌客&發表於2021-09-08

元表

Lua語言中的每種型別的值都有一套可預見的操作集合。例如,我們可以將數字相加,可以連線字串,還可以在表中插入鍵值對等,但是我們無法將兩個表相加,無法對函式作比較,也無法呼叫一個字串,除非使用元表。

元表可以修改一個值在面對一個未知操作時的行為。例如,假設a和b都是表,那麼可以通過元表定義Lua語言如何計算表示式a+b。當Lua語言試圖將兩個表相加時,它會先檢查兩者之一是否有元表(metatable)且該元表中是否有__add欄位。如果Lua語言找到了該欄位,就呼叫該欄位對應的值,即所謂的元方法(metamethod)(是一個函式)。

Lua語言中的每一個值都可以有元表。每一個表和使用者資料型別都具有各自獨立的元表,而其他型別的值則共享對應型別所屬的同一個元表。

獲取元表

獲取元表使用getmetatable()方法

t = {}
print(getmetatable(t)) --> nil

設定元表

可以使用函式setmetatable來設定或修改任意表的元表

t1 = {}
setmetatable(t,t1)
print(getmetatable(t) == t1) --> true

我們只能為表設定元表;如果要為其他型別的值設定元表,則必須通過C程式碼或除錯庫完成。字串標準庫為所有的字串都設罝了同一個元表,而其他型別在預設情況中都沒有元表:

print(getmetatable("hello world"))   --> table: 0000022E8BA91B40
print(getmetatable(123)) --> nil
print(getmetatable(print)) --> nil

為兩個表新增算術運算功能

下面展示怎麼為兩個表新增 “+” 功能

-- 定義兩個表 t 和 t1
t = {}
t1 = {}

-- 向表中新增變數並賦值
t.a = 123
t1.a = 4

-- 向兩個表新增函式
t.func = function ()
   return 1
end

t1.func = function ()
   return 2
end

-- 定義元表
mt = {}

-- 分別為兩個表設定元表
setmetatable(t,mt)
setmetatable(t1,mt)

--為元表定於__add函式
--a b 分別代表執行加法的表,這裡指代t和t1
mt.__add = function (a,b)
   print(a.func() + b.func())   --> 3
   return a.a + b.a
end

print(t + t1) --> 127

Lua語言會按照如下步驟來查詢元方法:

  • 如果第一個值有元表且元表中存在所需的元方法,那麼Lua語言就使用這個元方法,與第二個值無關

  • 如果第二個值有元表且元表中存在所需的元方法,Lua語言就使用這個元方法

  • 否則,Lua語言就丟擲異常。

因此 在執行最後一行 t + t1的時候,會檢查元表中是否存在 t1 中是否存在 __add 方法,如果存在,則呼叫該元方法,否則查詢 t2,如果還是不存在,將會丟擲異常。因此上面的程式碼中,這行程式碼 setmetatable(t1,mt) 可以刪除,因為始終會執行 t 中的方法。例如我們修改上面程式碼

-- 定義第二個元表
mt1 = {}

-- 註釋t的元表,為t1新增新定義的元表
--setmetatable(t,mt)
setmetatable(t1,mt1)

mt1.__add = function()
   print("this is mt1 add")
end

print(t + t1)  -->   呼叫mt1.__add方法,執行方法中的print語句
 -->   列印nil,因為mt1.__addm

除了 __add 外,還有下面這些鍵值定義:

描述
__add 改變加法操作符的行為。
__sub 改變減法操作符的行為。
__mul 改變乘法操作符的行為。
__div 改變除法操作符的行為。
__mod 改變模除操作符的行為。
__unm 改變一元減操作符的行為。
__concat 改變連線操作符的行為。
__eq 改變等於操作符的行為。
__lt 改變小於操作符的行為。
__le 改變小於等於操作符的行為。

 

表相關的元方法

__index元方法

當我們訪問表中一個不存在的欄位時,得到的結果會是nil,這是正確的,但不是完整的真相。實際上,這些訪問會引發直譯器查詢一個名為 __index 的元方法。如果沒有這個元方法,那麼像一般情況下一樣,結果就是nil;否則,則由這個元方法來提供最終結果。

mt = {x = 5}    --定義一個元表,裡面擁有一個欄位x

w = {}

w = setmetatable(w,mt) --將mt設定為w的元表

mt.__index = mt --設定元表的__index值

print(w.x) -- 5
print(w.y) -- nil

--將mt的__index元方法設定為函式
mt.__index = function(_,key)
   return mt[key]
end

print(w.x) --w中沒有x欄位,所以呼叫函式 __index,傳入的引數為function(w,x),所以得到的值為mt["x"] = 5
 --也就是以表和鍵為引數呼叫該函式,並返回該函式的返回值

Lua 查詢一個表元素時的規則,其實就是如下 3 個步驟:

  • 在表中查詢,如果找到,返回該元素,找不到則繼續

  • 判斷該表是否有元表,如果沒有元表,返回 nil,有元表則繼續。

  • 判斷元表有沒有 index 方法,如果 index 方法為 nil,則返回 nil;如果 index 方法是一個表,則重複 1、2、3;如果 index 方法是一個函式,Lua會以表和鍵為引數呼叫該函式,並返回該函式的返回值。

如果我們希望在訪問一個表時不呼叫__index元方法,那麼可以使用函式rawget,它在不考慮元表的情況下對錶進行簡單的訪問,定義為:

rawget (table, index)

在不觸發任何元方法的情況下 獲取 table[index] 的值。 table 必須是一張表; index 可以是任何值。

接著上面的程式碼,新增一些內容

print(rawget(w,"x"))    -- 忽略元表中的值,x在w表中不存在,所以輸出為 nil
w.x = "hhh"
print(rawget(w,"x"))    -- 輸出 hhh

 

__newindex元方法

元方法__newindex__index類似,不同之處在於前者用於表的更新而後者用於表的查詢。當對一個表中不存在的索引賦值時,直譯器就會查詢__newindex元方法:如果這個元方法存在,那麼直譯器就呼叫它而不執行賦值。

mt = {x = 5,y = 6}

w = {}

w = setmetatable(w,mt)
mt.__newindex = mt

print(mt.y)   -- 6
w.y = 123    -- __newindex中包含y欄位,所以為mt.y賦值,而不進行自身賦值
print(w.y) --nil
print(mt.y) --123

如果我們想跳過原函式為它賦值,可以使用rawset方法

rawset (table, index, value)

在不觸發任何元方法的情況下 將 table[index] 設為 valuetable 必須是一張表, index 可以是 nil 與 NaN 之外的任何值。 value 可以是任何 Lua 值。

在上面的基礎上新增下面程式碼

rawset(w,"y",456)
print(w.y) -- 456
print(mt.y) -- 123

 

相關文章