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