一文讀懂Lua元表


元表

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

 


免責聲明!

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



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