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]
設為value
。table
必須是一張表,index
可以是nil
與 NaN 之外的任何值。value
可以是任何Lua
值。
在上面的基礎上新增下面程式碼
rawset(w,"y",456)
print(w.y) -- 456
print(mt.y) -- 123