table 作為 Lua 中唯一的數(shù)據(jù)結(jié)構(gòu)喊崖,我們可以利用 table 實現(xiàn)面向?qū)ο缶幊讨械念惱染怠⒗^承、多重繼承等等宁脊。在這就介紹一下和 table 密切相關(guān)的 Lua 元表和元方法脾猛。
Lua 中的每個值都有一個元表撕彤。table 和 userdata 可以有各自獨立的元表,而其他類型的值則共享其類型所屬的單一元表猛拴。任何 table 都可以作為任何值的元表羹铅,而一組相關(guān)的 table 也可以共享一個通用的元表。一個 table 甚至可以作為它自己的元表愉昆。
通過 getmetatable
方法可以獲取一個值的元表睦裳,而 setmetatable
方法則可以設(shè)置一個值的元表。
t = {}
print(getmetatable(t)) --> nil
t1 = {}
setmetatable(t, t1)
assert(getmetatable(t) == t1)
t2 = {}
setmetatable(t2, t2)
assert(getmetatable(t2) == t2)
在 Lua 代碼中撼唾,只能設(shè)置 table 的元表。若要設(shè)置其他類型的值的元表哥蔚,則必須通過 C 代碼來完成倒谷。從下面的代碼也可以看出 Lua 中的所有字符串值是共用一個元表的。
print(getmetatable("hi")) --> table: 0x7fd0b14074b0
print(getmetatable("hello")) --> table: 0x7fd0b14074b0
print(getmetatable(10)) --> nil
print(getmetatable(false)) --> nil
print(getmetatable(function () end)) --> nil
setmetatable("hi", {}) --> error:bad argument #1 to 'setmetatable' (table expected, got string)
元表和元方法
關(guān)于 Lua 的元表和元方法糙箍,在云風(fēng)翻譯的 Lua 5.3 參考手冊 中有以下描述:
Lua 中的每個值都可以有一個元表渤愁。這個 元表 就是一個普通的 Lua 表,它用于定義原始值在特定操作下的行為深夯。如果你想改變一個值在特定操作下的行為抖格,你可以在它的元表中設(shè)置對應(yīng)域诺苹。例如,當(dāng)你對非數(shù)字值做加操作時雹拄,Lua 會檢查該值的元表中的 "
__add
" 域下的函數(shù)收奔。如果能找到,Lua 則調(diào)用這個函數(shù)來完成加這個操作滓玖。元表中的鍵對應(yīng)著不同的 事件 名坪哄;鍵關(guān)聯(lián)的那些值被稱為 元方法。在上面那個例子中引用的事件為
"add"
势篡,完成加操作的那個函數(shù)就是元方法翩肌。你可以用
getmetatable
函數(shù)來獲取任何值的元表。使用
setmetatable
來替換一張表的元表禁悠。在 Lua 中念祭,你不可以改變表以外其它類型的值的元表(除非你使用調(diào)試庫(參見§6.10));若想改變這些非表類型的值的元表碍侦,請使用 C API粱坤。表和完全用戶數(shù)據(jù)有獨立的元表(當(dāng)然,多個表和用戶數(shù)據(jù)可以共享同一個元表)祝钢。其它類型的值按類型共享元表比规;也就是說所有的數(shù)字都共享同一個元表,所有的字符串共享另一個元表等等拦英。默認(rèn)情況下蜒什,值是沒有元表的,但字符串庫在初始化的時候為字符串類型設(shè)置了元表(參見 §6.4)疤估。
元表決定了一個對象在數(shù)學(xué)運算灾常、位運算、比較铃拇、連接钞瀑、取長度、調(diào)用慷荔、索引時的行為雕什。元表還可以定義一個函數(shù),當(dāng)表對象或用戶數(shù)據(jù)對象在垃圾回收(參見§2.5)時調(diào)用它显晶。
接下來會給出一張元表可以控制的事件的完整列表贷岸。每個操作都用對應(yīng)的事件名來區(qū)分。每個事件的鍵名用加有 '
__
' 前綴的字符串來表示磷雇;例如 "add" 操作的鍵名為字符串 "__add
"偿警。注意、Lua 從元表中直接獲取元方法唯笙;訪問元表中的元方法永遠(yuǎn)不會觸發(fā)另一次元方法螟蒸。下面的代碼模擬了 Lua 從一個對象obj
中獲取一個元方法的過程:rawget(getmetatable(obj) or {}, "__" .. event_name)
盒使。對于一元操作符(取負(fù)、求長度七嫌、位反)少办,元方法調(diào)用的時候,第二個參數(shù)是個啞元抄瑟,其值等于第一個參數(shù)凡泣。這樣處理僅僅是為了簡化 Lua 的內(nèi)部實現(xiàn)(這樣處理可以讓所有的操作都和二元操作一致),這個行為有可能在將來的版本中移除皮假。(使用這個額外參數(shù)的行為都是不確定的鞋拟。)
- **"add": **
+
操作。如果任何不是數(shù)字的值(包括不能轉(zhuǎn)換為數(shù)字的字符串)做加法惹资,Lua 就會嘗試調(diào)用元方法贺纲。首先、Lua 檢查第一個操作數(shù)(即使它是合法的)褪测,如果這個操作數(shù)沒有為 "__add
" 事件定義元方法猴誊,Lua 就會接著檢查第二個操作數(shù)。一旦 Lua 找到了元方法侮措,它將把兩個操作數(shù)作為參數(shù)傳入元方法懈叹,元方法的結(jié)果(調(diào)整為單個值)作為這個操作的結(jié)果。如果找不到元方法分扎,將拋出一個錯誤澄成。- **"sub": **
-
操作。行為和 "add" 操作類似畏吓。- **"mul": **
*
操作墨状。行為和 "add" 操作類似。- **"div": **
/
操作菲饼。行為和 "add" 操作類似肾砂。- **"mod": **
%
操作。行為和 "add" 操作類似宏悦。- **"pow": **
^
(次方)操作镐确。行為和 "add" 操作類似。- **"unm": **
-
(取負(fù))操作饼煞。行為和 "add" 操作類似辫塌。- **"idiv": **
//
(向下取整除法)操作。行為和 "add" 操作類似派哲。- **"band": **
&
(按位與)操作。行為和 "add" 操作類似掺喻,不同的是 Lua 會在任何一個操作數(shù)無法轉(zhuǎn)換為整數(shù)時(參見 §3.4.3)嘗試取元方法芭届。- **"bor": **
|
(按位或)操作储矩。行為和 "band" 操作類似。- **"bxor": **
~
(按位異或)操作褂乍。行為和 "band" 操作類似持隧。- **"bnot": **
~
(按位非)操作。行為和 "band" 操作類似逃片。- **"shl": **
<<
(左移)操作屡拨。行為和 "band" 操作類似。- **"shr": **
>>
(右移)操作褥实。行為和 "band" 操作類似呀狼。- **"concat": **
..
(連接)操作。行為和 "add" 操作類似损离,不同的是 Lua 在任何操作數(shù)即不是一個字符串也不是數(shù)字(數(shù)字總能轉(zhuǎn)換為對應(yīng)的字符串)的情況下嘗試元方法哥艇。- **"len": **
#
(取長度)操作。如果對象不是字符串僻澎,Lua 會嘗試它的元方法貌踏。如果有元方法,則調(diào)用它并將對象以參數(shù)形式傳入窟勃,而返回值(被調(diào)整為單個)則作為結(jié)果祖乳。如果對象是一張表且沒有元方法,Lua 使用表的取長度操作(參見 §3.4.7)秉氧。其它情況眷昆,均拋出錯誤。- **"eq": **
==
(等于)操作谬运。和 "add" 操作行為類似隙赁,不同的是 Lua 僅在兩個值都是表或都是完全用戶數(shù)據(jù)且它們不是同一個對象時才嘗試元方法。調(diào)用的結(jié)果總會被轉(zhuǎn)換為布爾量梆暖。- **"lt": **
<
(小于)操作伞访。和 "add" 操作行為類似,不同的是 Lua 僅在兩個值不全為整數(shù)也不全為字符串時才嘗試元方法轰驳。調(diào)用的結(jié)果總會被轉(zhuǎn)換為布爾量厚掷。- **"le": **
<=
(小于等于)操作。和其它操作不同级解,小于等于操作可能用到兩個不同的事件冒黑。首先,像 "lt" 操作的行為那樣勤哗,Lua 在兩個操作數(shù)中查找 "__le
" 元方法抡爹。如果一個元方法都找不到,就會再次查找 "__lt
" 事件芒划,它會假設(shè)a <= b
等價于not (b < a)
冬竟。而其它比較操作符類似欧穴,其結(jié)果會被轉(zhuǎn)換為布爾量。- **"index": **索引
table[key]
泵殴。當(dāng)table
不是表或是表table
中不存在key
這個鍵時涮帘,這個事件被觸發(fā)。此時笑诅,會讀出table
相應(yīng)的元方法调缨。盡管名字取成這樣,這個事件的元方法其實可以是一個函數(shù)也可以是一張表吆你。如果它是一個函數(shù)弦叶,則以table
和key
作為參數(shù)調(diào)用它。如果它是一張表早处,最終的結(jié)果就是以key
取索引這張表的結(jié)果湾蔓。(這個索引過程是走常規(guī)的流程,而不是直接索引砌梆,所以這次索引有可能引發(fā)另一次元方法默责。)- **"newindex": **索引賦值
table[key] = value
。和索引事件類似咸包,它發(fā)生在table
不是表或是表table
中不存在key
這個鍵的時候桃序。此時,會讀出table
相應(yīng)的元方法烂瘫。同索引過程那樣媒熊,這個事件的元方法即可以是函數(shù),也可以是一張表坟比。如果是一個函數(shù)芦鳍,則以table
、key
葛账、以及value
為參數(shù)傳入柠衅。如果是一張表,Lua 對這張表做索引賦值操作籍琳。(這個索引過程是走常規(guī)的流程菲宴,而不是直接索引賦值,所以這次索引賦值有可能引發(fā)另一次元方法趋急。)一旦有了 "newindex" 元方法喝峦,Lua 就不再做最初的賦值操作。(如果有必要呜达,在元方法內(nèi)部可以調(diào)用rawset
來做賦值谣蠢。)- **"call": **函數(shù)調(diào)用操作
func(args)
。當(dāng) Lua 嘗試調(diào)用一個非函數(shù)的值的時候會觸發(fā)這個事件(即func
不是一個函數(shù))。查找func
的元方法漩怎,如果找得到勋颖,就調(diào)用這個元方法,func
作為第一個參數(shù)傳入勋锤,原來調(diào)用的參數(shù)(args
)后依次排在后面。
算術(shù)類的元方法:__add
(加法)侥祭、__mul
(乘法)叁执、__sub
(減法)、__div
(除法)矮冬、__unm
(相反數(shù))谈宛、__mod
(取模)、__pow
(乘冪)胎署。
關(guān)系類的元方法:__eq
(等于)吆录、__lt
(小于)、__le
(小于等于)琼牧。其他的關(guān)系操作符則沒有單獨的元方法恢筝,Lua 會將 a ~= b
轉(zhuǎn)換為 not a == b
,將 a > b
轉(zhuǎn)換為 a < b
巨坊,將 a >= b
轉(zhuǎn)換為 a <= b
撬槽。
庫定義的元方法:__tostring
、__metatable
趾撵。
函數(shù) print 總是調(diào)用 tostring 來格式化其輸出侄柔。當(dāng)格式化任意值時,tostring 會檢查該值是否有一個 __tostring
的元方法占调。如果有這個元方法暂题,tostring 就用該值作為參數(shù)來調(diào)用這個元方法,該元方法的返回值就是 tostring 的結(jié)果究珊。
函數(shù) setmetatable 和 getmetatable 會觸發(fā) __metatable
元方法薪者。當(dāng) Lua 中的值擁有該元方法時,getmetatable 就會返回這個字段的值苦银,而 setmetatable 則會引發(fā)一個錯誤啸胧。因此我們可以使用 __metatable
元方法來保護(hù)任意值的元表,這樣值的元表就不會被隨意修改了幔虏。
t = {}
mt = {}
mt.__metatable = "not your business"
setmetatable(t, mt)
print(getmetatable(t)) --> not your business
setmetatable(t, {}) --> error:cannot change a protected metatable
table 訪問的元方法:__index
纺念、__newindex
。
算術(shù)類和關(guān)系類的元方法
算術(shù)類和關(guān)系類的元方法類似于其他編程語言中的操作符重載想括,我們可以利用元方法來實現(xiàn)任何不是數(shù)字的值(包括不能轉(zhuǎn)換為數(shù)字的字符串)的算術(shù)和關(guān)系運算陷谱。
local mt = {}
mt.__add = function (a, b)
print("call mt.__add")
return {x = a.x + b.x, y = a.y + b.y}
end
mt.__eq = function (a, b)
print("call mt.__eq")
return a.x == b.x and a.y == b.y
end
mt.__tostring = function (point)
print("call mt.__tostring")
return string.format("[x = %f, y = %f]", point.x, point.y)
end
Point = {}
function Point.new(x, y)
local point = {x = x, y = y}
setmetatable(point, mt)
return point
end
local p1 = Point.new(10, 10)
local p2 = Point.new(20, 20)
print(p1)
print(tostring(p2))
print("----------")
local p3 = p1 + p2
print(p3)
print("----------")
print(p1 == p2)
print("----------")
print(p1 ~= p2)
執(zhí)行以上代碼輸出如下:
call mt.__tostring
[x = 10.000000, y = 10.000000]
call mt.__tostring
[x = 20.000000, y = 20.000000]
----------
call mt.__add
table: 0x7fd462504e10
----------
call mt.__eq
false
----------
call mt.__eq
true
最后
在這只是簡單介紹了 Lua 中的元表和元方法的概念,以及算術(shù)類和關(guān)系類的元方法的使用。但其實 table 訪問的元方法 __index
和 __newindex
才是在 Lua 實現(xiàn)面向?qū)ο缶幊痰年P(guān)鍵烟逊,這個會在下一篇文章中介紹渣窜。
本文出自 Eddy Wiki ,轉(zhuǎn)載請注明出處:http://eddy.wiki/lua-metatable.html