Lua 元表和元方法

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ù)弦叶,則以 tablekey 作為參數(shù)調(diào)用它。如果它是一張表早处,最終的結(jié)果就是以 key 取索引這張表的結(jié)果湾蔓。(這個索引過程是走常規(guī)的流程,而不是直接索引砌梆,所以這次索引有可能引發(fā)另一次元方法默责。)
  • **"newindex": **索引賦值 table[key] = value 。和索引事件類似咸包,它發(fā)生在table 不是表或是表 table 中不存在key 這個鍵的時候桃序。此時,會讀出 table 相應(yīng)的元方法烂瘫。同索引過程那樣媒熊,這個事件的元方法即可以是函數(shù),也可以是一張表坟比。如果是一個函數(shù)芦鳍,則以 tablekey葛账、以及 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宪躯,一起剝皮案震驚了整個濱河市乔宿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌访雪,老刑警劉巖详瑞,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異臣缀,居然都是意外死亡坝橡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門精置,熙熙樓的掌柜王于貴愁眉苦臉地迎上來计寇,“玉大人,你說我怎么就攤上這事脂倦》” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵狼讨,是天一觀的道長贝淤。 經(jīng)常有香客問我,道長政供,這世上最難降的妖魔是什么播聪? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮布隔,結(jié)果婚禮上离陶,老公的妹妹穿的比我還像新娘。我一直安慰自己衅檀,他們只是感情好招刨,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著哀军,像睡著了一般沉眶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上杉适,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天谎倔,我揣著相機與錄音,去河邊找鬼猿推。 笑死片习,一個胖子當(dāng)著我的面吹牛捌肴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播藕咏,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼状知,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了孽查?” 一聲冷哼從身側(cè)響起饥悴,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎盲再,沒想到半個月后铺坞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡洲胖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年粹胯,在試婚紗的時候發(fā)現(xiàn)自己被綠了沪悲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡骂蓖,死狀恐怖腐晾,靈堂內(nèi)的尸體忽然破棺而出叉弦,到底是詐尸還是另有隱情,我是刑警寧澤藻糖,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布淹冰,位于F島的核電站,受9級特大地震影響巨柒,放射性物質(zhì)發(fā)生泄漏樱拴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一洋满、第九天 我趴在偏房一處隱蔽的房頂上張望晶乔。 院中可真熱鬧,春花似錦牺勾、人聲如沸正罢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽翻具。三九已至,卻和暖如春回还,著一層夾襖步出監(jiān)牢的瞬間裆泳,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工懦趋, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留晾虑,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像帜篇,于是被迫代替她去往敵國和親糙捺。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容