元表
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
- 一旦有了"newindex"元方法,Lua就不再做最初的賦值操作;
- "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" = {
- }
- }
]]