Lua之元表和元方法

元表

Lua中的每個值都可以有一個元表!這個元表就是一個普通的Lua表,它用於定義原始值在特定操作下的行爲。
如果你想改變一個值在特定操作下的行爲,你可以在它的元表中設置對應域。例如當你對非數字值做加操作時,Lua會檢查該值的元表中的"__add"域下的函數。如果能找到,Lua則調用這個函數來完成加這個操作。
在Lua中,你不可以改變表以外其它類型的值的元表(除非你使用調試庫),若想改變這些非表類型的值的元表,請使用C API。
表和完全用戶數據有獨立的元表(當然,多個表和用戶數據可以共享同一個元表)。其它類型的值按類型共享元表,也就是說所有的數字都共享同一個元表,所有的字符串共享另一個元表等等。默認情況下,值是沒有元表的,但字符串庫在初始化的時候爲字符串類型設置了元表。
元表決定了一個對象在數學運算、位運算、比較、連接、取長度、調用、索引時的行爲。元表還可以定義一個函數,當表對象或用戶數據對象在垃圾回收時調用它。

使用getmetatable函數來獲取任何值的元表,使用setmetatable來替換一張表的元表。

local testStr = "hello, fightsyj"
local testTbl = {name = "fightsyj", age = 666}
-- getmetatable獲取對象的元表
print("testStr`s metatable is ->", getmetatable(testStr))  -- 字符串庫在初始化的時候爲字符串類型設置了元表
print("testTbl`s metatable is ->", getmetatable(testTbl))

local testMetaTbl = {}
-- setmetatable設置元表,返回設置元表之後的對象
local finalTbl = setmetatable(testTbl, testMetaTbl)
dump(finalTbl, "finalTbl->")
print("testTbl`s metatable is ->", getmetatable(testTbl))
--[[
testStr`s metatable is ->	table: 00AA9780
testTbl`s metatable is ->	nil
- "finalTbl->" = {
-     "age"  = 666
-     "name" = "fightsyj"
- }
testTbl`s metatable is ->	table: 00DF1E60
]]

元方法

元表中的鍵對應着不同的事件名,鍵關聯的那些值被稱爲元方法。在上面那個例子中引用的事件爲"add",完成加操作的那個函數就是元方法。
接下來會給出一張元表可以控制的事件的完整列表。每個操作都用對應的事件名來區分。每個事件的鍵名用加有"__“前綴的字符串來表示。例如"add"操作的鍵名爲字符串”__add"。需要注意的是Lua從元表中直接獲取元方法,訪問元表中的元方法永遠不會觸發另一次元方法。下面的代碼模擬了Lua從一個對象obj中獲取一個元方法的過程:
rawget(getmetatable(obj) or {}, “__” … event_name)
對於一元操作符(取負、求長度、位反),元方法調用的時候,第二個參數是個啞元,其值等於第一個參數。這樣處理僅僅是爲了簡化Lua的內部實現(這樣處理可以讓所有的操作都和二元操作一致),這個行爲有可能在將來的版本中移除。(使用這個額外參數的行爲都是不確定的)

元方法 描述
add + 操作。如果任何不是數字的值(包括不能轉換爲數字的字符串)做加法,Lua就會嘗試調用元方法。首先Lua檢查第一個操作數(即使它是合法的),如果這個操作數沒有爲"__add"事件定義元方法,Lua就會接着檢查第二個操作數。一旦Lua找到了元方法,它將把兩個操作數作爲參數傳入元方法,元方法的結果(調整爲單個值)作爲這個操作的結果。如果找不到元方法,將拋出一個錯誤。
sub - 操作。行爲和"add"操作類似。
mul * 操作。行爲和"add"操作類似。
div / 操作。行爲和"add"操作類似。
mod % 操作。行爲和"add"操作類似。
pow ^ (次方)操作。行爲和"add"操作類似。
unm - (取負)操作。行爲和"add"操作類似。
idiv // (向下取整除法)操作。行爲和"add"操作類似。
band & (按位與)操作。行爲和"add"操作類似,不同的是Lua會在任何一個操作數無法轉換爲整數時嘗試取元方法。
bor | (按位或)操作。行爲和"band"操作類似。
bxor ~ (按位異或)操作。行爲和"band"操作類似。
bnot ~ (按位非)操作。行爲和"band"操作類似。
shl << (左移)操作。行爲和"band"操作類似。
shr >> (右移)操作。行爲和"band"操作類似。
concat .. (連接)操作。行爲和"add"操作類似,不同的是Lua在任何操作數既不是一個字符串,也不是數字(數字總能轉換爲對應的字符串)的情況下嘗試元方法。
len # (取長度)操作。如果對象不是字符串Lua會嘗試它的元方法。如果有元方法,則調用它並將對象以參數形式傳入,而返回值(被調整爲單個)則作爲結果。如果對象是一張表且沒有元方法,Lua使用表的取長度操作。其它情況,均拋出錯誤。
eq == (等於)操作。和 “add” 操作行爲類似,不同的是Lua僅在兩個值都是表或都是完全用戶數據,且它們不是同一個對象時才嘗試元方法。調用的結果總會被轉換爲布爾量。
lt < (小於)操作。和"add"操作行爲類似,不同的是Lua僅在兩個值不全爲整數也不全爲字符串時才嘗試元方法。調用的結果總會被轉換爲布爾量。
le <= (小於等於)操作。和其它操作不同,小於等於操作可能用到兩個不同的事件。首先,像"lt"操作的行爲那樣,Lua在兩個操作數中查找"__le"元方法。如果一個元方法都找不到,就會再次查找"__lt"事件,它會假設a <= b等價於not (b < a)。而其它比較操作符類似,其結果會被轉換爲布爾量。
index 索引table[key]。當table不是表或是表table中不存在key這個鍵時,這個事件被觸發。此時,會讀出table相應的元方法。儘管名字取成這樣,這個事件的元方法其實可以是一個函數也可以是一張表。如果它是一個函數,則以table和key作爲參數調用它。如果它是一張表,最終的結果就是以key取索引這張表的結果。(這個索引過程是走常規的流程,而不是直接索引,所以這次索引有可能引發另一次元方法。)
newindex 索引賦值table[key] = value。和索引事件類似,它發生在table不是表或是表table中不存在key這個鍵的時候。此時,會讀出table相應的元方法。同索引過程那樣,這個事件的元方法既可以是函數,也可以是一張表。如果是一個函數,則以table、key以及value爲參數傳入。如果是一張表,Lua對這張表做索引賦值操作。(這個索引過程是走常規的流程,而不是直接索引賦值,所以這次索引賦值有可能引發另一次元方法。)一旦有了"newindex"元方法,Lua就不再做最初的賦值操作。(如果有必要,在元方法內部可以調用rawset來做賦值。)
call 函數調用操作func(args)。當Lua嘗試調用一個非函數的值的時候會觸發這個事件(即func不是一個函數)。查找func的元方法,如果找得到,就調用這個元方法,func作爲第一個參數傳入,原來調用的參數(args)後依次排在後面。
mode 弱表屬性,賦予一張表弱引用屬性。
gc 在對象被GC的時候,會先調用元表裏面的"gc"域。
tostring 當調用tostring(obj)的時候,會先查找obj的元方法中的"__tostring",如果有就調用,沒有就會打印obj的內存位置。
pairs 迭代器的元方法,在執行pairs(t)的時候,會先找表t的元方法"__pairs",如果有就以t爲參數調用他,如果沒有,就返回三個值next函數,t已經nil。
metatable 函數setmetatable和getmetatable會觸發"__metatable"元方法。當Lua中的值擁有該元方法時,getmetatable就會返回這個字段的值,而setmetatable則會引發一個錯誤。因此我們可以使用"__metatable"元方法來保護任意值的元表,這樣值的元表就不會被隨意修改了。

下面着重介紹一下__add、__index和__newindex這三個元方法。

__add

__add類似於C++中的運算符重載,對算術運算符+進行重載,重新定義+的操作行爲!

local testTbl1 = {name = "fightsyj", age = 666}
local testTbl2 = {sex = "Male"}
-- local testTbl3 = testTbl1 + testTbl2  -- 報錯
local testMetaTbl = {}
testMetaTbl.__add = function(tbl1, tbl2)
   for key, value in pairs(tbl2) do
   	tbl1[key] = value
   end
   return tbl1
end
setmetatable(testTbl1, testMetaTbl)
local testTbl3 = testTbl1 + testTbl2
dump(testTbl3)
--[[
- "<var>" = {
-     "age"  = 666
-     "name" = "fightsyj"
-     "sex"  = "Male"
- }
]]

__index

查詢表中不存在的元素時觸發!這個事件的元方法可以是一個函數也可以是一張表。如果它是一個函數,則以table和key作爲參數調用它。如果它是一張表,最終的結果就是以key取索引這張表的結果。

  • __index方法是一個表
local testTbl = {name = "fightsyj", age = 666}
local testMetaTbl = {}
testMetaTbl.__index = {sex = "Male"}
setmetatable(testTbl, testMetaTbl)
print(testTbl.name, testTbl.sex, testTbl.place)
-- fightsyj 	Male 	nil
  • __index方法是一個函數
local testTbl = {name = "fightsyj", age = 666}
local testMetaTbl = {}
testMetaTbl.__index = function(tbl, key)
   return string.format("%s is not exist !", key)
end
setmetatable(testTbl, testMetaTbl)
print(testTbl.name)
print(testTbl.sex)
--[[
fightsyj
sex is not exist !
]]

Lua查找一個表元素時的規則,其實就是如下3個步驟:
1.在表中查找,如果找到,返回該元素,找不到則繼續;
2.判斷該表是否有元表,如果沒有元表,返回nil,有元表則繼續;
3.判斷元表有沒有__index方法,如果__index方法爲nil,則返回nil;如果__index方法是一個表,則重複 1、2、3;如果__index方法是一個函數,則返回該函數的返回值。

__newindex

更新表中不存在的元素時觸發!這個事件的元方法既可以是函數,也可以是一張表。如果是一個函數,則以table、key以及value作爲參數傳入。如果是一張表,則對這張表做索引賦值操作。

  • __newindex方法是一個表
local testTbl = {name = "fightsyj"}
local newindex = {}
local testMetaTbl = {__newindex = newindex}
setmetatable(testTbl, testMetaTbl)
testTbl.name = "fightsyj2"
testTbl.age = 666
dump(testTbl, "testTbl->")
dump(testMetaTbl, "testMetaTbl->")
--[[
- "testTbl->" = {
-     "name" = "fightsyj2"
- }
- "testMetaTbl->" = {
-     "__newindex" = {
-         "age" = 666
-     }
- }
]]
  • __newindex方法是一個函數
local testTbl = {name = "fightsyj"}
local newindex = function(tbl, key, value)
	print(tbl, key, value)
end
local testMetaTbl = {__newindex = newindex}
setmetatable(testTbl, testMetaTbl)
testTbl.age = 666
dump(testTbl, "testTbl->")
dump(testMetaTbl, "testMetaTbl->")
--[[
table: 00A291E0	age	666
- "testTbl->" = {
-     "name" = "fightsyj"
- }
- "testMetaTbl->" = {
-     "__newindex" = function: 00A32688
- }
]]

ps

  1. 一旦有了"newindex"元方法,Lua就不再做最初的賦值操作;
  2. "newindex"元方法用來對錶進行更新(類似set),"index"元方法則用來對錶進行查詢(類似get);

忽略元表:rawget和rawset

有時候我們希望直接改動或獲取表中的值時,就需要rawget和rawset方法了。

rawget

rawget可以讓你直接獲取到表中索引的實際值,而不通過元表的__index元方法。

local testTbl = {name = "fightsyj", age = 666}
local testMetaTbl = {}
testMetaTbl.__index = {sex = "Male"}
setmetatable(testTbl, testMetaTbl)
-- 通過rawget直接獲取testTbl中sex對應的值,不會觸發元表的__index事件
local sexValue = rawget(testTbl, "sex")
print(sexValue)  -- nil

rawset

rawset可以讓你直接爲表中索引的賦值,而不通過元表的__newindex元方法。

local testTbl = {name = "fightsyj"}
local newindex = {}
local testMetaTbl = {__newindex = newindex}
setmetatable(testTbl, testMetaTbl)
-- 通過rawset直接對testTbl進行賦值操作,不會觸發元表的__newindex事件
rawset(testTbl, "age", 666)
dump(testTbl, "testTbl->")
dump(testMetaTbl, "testMetaTbl->")
--[[
- "testTbl->" = {
-     "age"  = 666
-     "name" = "fightsyj"
- }
- "testMetaTbl->" = {
-     "__newindex" = {
-     }
- }
]]

參考:
Lua 5.3 參考手冊
Lua查找表元素過程
Lua中的元表與元方法學習總結

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章