Lua 元表簡(jiǎn)述

補(bǔ)充一下metatable (元表)的知識(shí)庸毫。
lua中的table擁有一些列可預(yù)見的操作冠跷。
比如申明一個(gè)普通的table:a = {x=1爹凹,y=2}厨诸,如果我們?cè)L問(wèn)a.x,那么打印1禾酱,如果我們?cè)L問(wèn)a.y,那么打印2微酬,如果我們?cè)L問(wèn)a.z,那么打印nil。但是如果我們想要a.z有值呢颤陶?當(dāng)然不是簡(jiǎn)單的a.z = xx賦值這么簡(jiǎn)單的方法颗管。這時(shí)候就需要我們的元表了。
lua提供了內(nèi)建函數(shù)getmetatablesetmetatable
lua的元表可以是自身滓走,也可以是別的table:

> a = {x=1,y=2}
> setmetatable(a,a) -- 設(shè)置a的元表為自己
table: 003ea930 -- 返回table a
> getmetatable(a) -- 返回a的元表
table: 003ea930
> b = {z=3}
> setmetatable(a,b) -- 設(shè)置a的元表為b
table: 003ea930
> getmetatable(a)
table: 003eb328
> a.z -- 猜猜看我們能打印出3嗎垦江?

好,既然上面不能打印3那就疑問(wèn)了搅方,我們?cè)O(shè)置這個(gè)干啥呢比吭?這個(gè)需要配合元方法使用!上面這個(gè)例子后面還會(huì)說(shuō)姨涡,我們先來(lái)看最簡(jiǎn)單的元方法-算術(shù)元方法衩藤。
舉個(gè)例子,通常我們想要兩個(gè)table實(shí)現(xiàn)+運(yùn)算是不行的:

> a = {1}
> b = {2}
> a + b
stdin:1: attempt to perform arithmetic on a table value (global 'a')
stack traceback:
        stdin:1: in main chunk
        [C]: in ?

好涛漂,下面我們來(lái)定義一個(gè)集合赏表,保存為set.lua文件:

Set = {}

Set.mt = {}

function Set.new(t)
    local set = {}
    setmetatable(set,Set.mt)
    for _,v in pairs(t) do 
        table.insert(set,v) 
    end
    return set
end

function Set.add(a,b)
    local ret = Set.new{} -- 等價(jià)于 Set.new({}),以前提過(guò)
    for _,v in pairs(a) do 
        table.insert(ret,v)  
    end
    for _,v in pairs(b) do 
        table.insert(ret,v)  
    end
    return ret
end

Set.mt.__add = Set.add -- 注意這里的 __add

我們引入這個(gè)模塊,然后執(zhí)行我們的加法:

> require "set"
true
> s1 = Set.new{1,2}
> s2 = Set.new{4,5}
> s3 = s1 + s2
> for k,v in pairs(s3) do print(k,v) end
1       1
2       2
4       4
5       5

到這里,我們發(fā)現(xiàn)我們可以加法了呢怖喻。底哗。岁诉。為啥呢锚沸??涕癣?
仔細(xì)看我們上面的元表Set.mt哗蜈,我們對(duì)它增加了一個(gè)方法Set.mt.__add,如果我們嘗試把它賦值為nil:

> Set.mt.__add = nil
> s1 + s2 -- 報(bào)錯(cuò)了坠韩。
stdin:1: attempt to perform arithmetic on a table value (global 's1')
stack traceback:
        stdin:1: in main chunk
        [C]: in ?
> Set.mt.__add = Set.add -- 這里再改回來(lái)距潘!

所以呢,我們兩個(gè)Set能相加只搁,多虧了我們的__add方法音比。我們稱之為元方法,類似的方法還有很多:

更多重載

另外氢惋,lua對(duì)于自定義類型(table)的操作運(yùn)算遵循:

  1. 如果第一個(gè)值有元表洞翩,并且有對(duì)應(yīng)的操作符重載稽犁,lua選擇它作為這次運(yùn)算的方法。不依賴第二個(gè)值
  2. 否則骚亿,如果第二個(gè)值有元表已亥,并且有對(duì)應(yīng)的操作符重載,lua選擇它作為這次運(yùn)算的方法来屠。
  3. 否則虑椎,lua raises an error(報(bào)錯(cuò))

按照上面的規(guī)則,1+s1s1+1是等價(jià)的俱笛,想一想為什么捆姜?

如果我們直接去運(yùn)算 s1+1會(huì)報(bào)錯(cuò),我們需要加條件判斷:

function Set.add(a,b)
    -- 增加對(duì)a和b的判斷
    if getmetatable(a) ~= Set.mt or getmetatable(b) ~= Set.mt then
        error("attempt to `add' a set with a non-set value")
    end

    local ret = Set.new{} -- 等價(jià)于 Set.new({})
    for _,v in pairs(a) do 
        table.insert(ret,v)  
    end
    for _,v in pairs(b) do 
        table.insert(ret,v)  
    end
    return ret
end

再試一下:

> s1 + 1 -- 現(xiàn)在就是報(bào)我們自己的error了
.\set.lua:17: attempt to `add' a set with a non-set value
stack traceback:
        [C]: in function 'error'
        .\set.lua:17: in metamethod '__add'
        stdin:1: in main chunk
        [C]: in ?

還有關(guān)系元方法:

__eq 重載 ==  -- 沒(méi)有~=,可以用 not ==
__lt 重載 <   -- 沒(méi)有>迎膜,可以換個(gè)方向 
__le 重載 <=  -- 沒(méi)有>=娇未,可以換個(gè)方向 

改造一下我們的腳本:

function Set.check(a,b)
    -- 增加對(duì)a和b的判斷
    if getmetatable(a) ~= Set.mt or getmetatable(b) ~= Set.mt then
        error("attempt to operate a set with a non-set value")
    end
end

function Set.le(a,b)
    Set.check(a,b)
    return #a <= #b
end


function Set.lt(a,b)
    return a <= b and not (b <= a)
end

function Set.eq(a,b)
    return a <= b and b <= a
end

Set.mt.__le = Set.le
Set.mt.__lt = Set.lt
Set.mt.__eq = Set.eq

退出重新引入一下:

> Set.new{1,2,3} == Set.new{4,5,6} -- 長(zhǎng)度一致
true
> {1,2,3} == {4,5,6} -- 沒(méi)有重載就會(huì)去判斷對(duì)象是否同一個(gè)
false
> {1,2,3} == {1,2,3} -- 這里跟python不同
false

關(guān)系元方法跟算術(shù)元方法不一樣,它不支持混合類型星虹。如果你比較兩個(gè)不同類型的數(shù)據(jù)(string和number)的大小零抬,或者擁有不同關(guān)系元方法的對(duì)象的大小,lua將報(bào)錯(cuò)宽涌,但是==是另外平夜。==的兩邊如果是不同類型的數(shù)據(jù),那么直接返回false卸亮,或者都是table忽妒,但是他們的關(guān)系元方法不一樣,那么也是返回false兼贸,僅當(dāng)都是對(duì)象(table)段直,且關(guān)系元方法一致,那么lua將會(huì)調(diào)用這個(gè)元方法溶诞。

還有一些其他的元方法:

__tostring  重載內(nèi)建函數(shù) tostring()
__metatable 重載內(nèi)建函數(shù) getmetatable()

看例子:
對(duì)我們的腳本增加下面代碼:

function Set.checkset(a)
    if getmetatable(a) ~= Set.mt then
        error("attempt to operate with a non-set value")
    end
end

function Set.tostring(a)
    Set.checkset(a)
    local ret = "{"
    local sep = ""
    for k,v in pairs(a) do 
        ret=ret..sep..v
        sep = ","
    end
    return ret.."}"
end

重新引入:

> s1 = Set.new{1,2,3}
> s1
{1,2,3} -- 這就是我們想要的打印.

-- 如果不想別人修改你的metatable,那么我們可以定義一個(gè)__metatable
> Set.mt.__metatable = "not your business"
> getmetatable(s1)
not your business
> setmetatable(s1,s1) 
stdin:1: cannot change a protected metatable
stack traceback:
        [C]: in function 'setmetatable'
        stdin:1: in main chunk
        [C]: in ?
 
-- 無(wú)法修改保護(hù)的metatable鸯檬,OK目的達(dá)成

以上基礎(chǔ)元方法已經(jīng)介紹完畢,最后看兩個(gè)特殊的元方法__index__newindex螺垢,它們就可以完成我們一開始的a.z任務(wù)_
所有的table喧务,它們?cè)谠L問(wèn)成員的時(shí)候其實(shí)通過(guò)__index
方法去找的,如果找到返回這個(gè)值枉圃,如果沒(méi)找到返回nil功茴。
先來(lái)看看我們的最新腳本文件set.lua:

Set = {}

Set.mt = {}
Set.mt.__metatable = "sorry abourt this"

function Set.new(t)
    local set = {}
    setmetatable(set,Set.mt)
    for _,v in pairs(t) do 
        table.insert(set,v) 
    end
    return set
end

function Set.checkset(a)
    if getmetatable(a) ~= Set.mt.__metatable then
        error("attempt to operate with a non-set value")
    end
end

function Set.check(a,b)
    -- 增加對(duì)a和b的判斷
    if getmetatable(a) ~= Set.mt.__metatable 
        or getmetatable(b) ~= Set.mt.__metatable then
        error("attempt to operate a set with a non-set value")
    end
end

function Set.add(a,b)
    Set.check(a,b)
    local ret = Set.new{} -- 等價(jià)于 Set.new({})
    for _,v in pairs(a) do 
        table.insert(ret,v)  
    end
    for _,v in pairs(b) do 
        table.insert(ret,v)  
    end
    return ret
end

function Set.le(a,b)
    Set.check(a,b)
    return #a <= #b
end


function Set.lt(a,b)
    return a <= b and not (b <= a)
end

function Set.eq(a,b)
    return a <= b and b <= a
end

function Set.tostring(a)
    Set.checkset(a)
    local ret = "{"
    local sep = ""
    for k,v in pairs(a) do 
        ret=ret..sep..v
        sep = ","
    end
    return ret.."}"
end


Set.mt.__add = Set.add
Set.mt.__le = Set.le
Set.mt.__lt = Set.lt
Set.mt.__eq = Set.eq
Set.mt.__tostring = Set.tostring

重新引入文件,然后看這個(gè)例子:

> s1 = Set.new{1,2,3} -- 申明一個(gè)新的Set
> s1 -- 看打印
{1,2,3}
-- 我們申明__index函數(shù)孽亲,打印參數(shù)table和key坎穿,并返回nil
> Set.mt.__index = function(tab,key) print(tab,key) return nil end
> s1.a -- 跟原來(lái)一樣
a
> s1.b -- 區(qū)別來(lái)了。。玲昧。
{1,2,3,a}       b
nil

通過(guò)上面的例子我們看到犯祠,當(dāng)調(diào)用s1里面含有的key的時(shí)候跟原來(lái)一樣,但是s1.b的時(shí)候酌呆,由于我們的s1里面并不含有衡载,所以就調(diào)用了我們給__index賦值的函數(shù),打印輸出隙袁,當(dāng)然結(jié)果還是nil.
回到最開始的例子(a.z):

> a = {x=1,y=2}
> b = {z=3}
> setmetatable(a,b)
table: 003eee58
> b.__index = function(tab,key) return b[key] end
> a.z
3 -- OK 我們想要的結(jié)果來(lái)了~

其實(shí)我們的__index可以是function也可以是table痰娱,如果是function,那么會(huì)把調(diào)用的table和key作為參數(shù)傳過(guò)去菩收;如果是table梨睁,那么lua只是重新查詢,如果查到返回娜饵,沒(méi)有則繼續(xù)調(diào)用這個(gè)table的__index元方法坡贺。
利用這一點(diǎn)我們可以完成類的繼承這樣的功能。具體后面再細(xì)說(shuō)箱舞。
如果我們只是想訪問(wèn)這個(gè)table的元素遍坟,避開它的__index調(diào)用,我們可以調(diào)用內(nèi)建函數(shù)rawget(table,key):

> rawget(a,"x") -- 避開__index
1
> rawget(a,"z") -- 避開__index
nil

rawget函數(shù)并不能給我們的程序提速晴股,但是我們有時(shí)候需要用它愿伴。

__newindex元方法是更新table,當(dāng)我們給table賦值一個(gè)本不存在的key的時(shí)候电湘,lua解釋器先去找這個(gè)table的__newindex隔节,如果不為nil,則調(diào)用它寂呛;如果為nil怎诫,就對(duì)table做賦值操作:

> a = {}
> setmetatable(a,a)
table: 0033aa88
> a.__newindex = function(tab,k,v) return print(tab,k,v) end
> a.x = 1
table: 0033aa88 x       1
>a.x
nil

__newindex的值可以是table,這樣賦值操作將會(huì)在那個(gè)table上發(fā)生贷痪,原table不會(huì)做任何事幻妓。

> a = {}
> b = {}
> setmetatable(a,b)
table: 00c6f0a0
> b.__newindex = b -- 想想這里能不能賦值 a ?
> a.x = 1
> a.x
nil
> b.x
1
> rawset(a,'y',2) -- 這個(gè)跟rawget有點(diǎn)類似呢诬,避開__newindex
table: 0068a9c0
> a.y
2
> b.y
nil
> b.__index = b
> a.x
1

table的默認(rèn)值是nil涌哲,我們可以用__index修改這個(gè)默認(rèn)值胖缤,想想可以怎么做尚镰?

利用__index__newindex我們可以監(jiān)視某個(gè)空table的操作:

-- create private index
local index = {}

-- create metatable
local mt = {
  __index = function (t,k)
    print("*access to element " .. tostring(k))
    return t[index][k]   -- access the original table
  end,

  __newindex = function (t,k,v)
    print("*update of element " .. tostring(k) ..
                         " to " .. tostring(v))
    t[index][k] = v   -- update original table
  end
}

function track (t)--監(jiān)視函數(shù),要監(jiān)視某個(gè)table,只需t=track(t)
  local proxy = {}
  proxy[index] = t
  setmetatable(proxy, mt)
  return proxy
end

把table變?yōu)橹蛔x:

function readOnly (t)
  local proxy = {}
  local mt = {       -- create metatable
    __index = t,
    __newindex = function (t,k,v)
      error("attempt to update a read-only table", 2)
    end
  }
  setmetatable(proxy, mt)
  return proxy
end

使用方法:

days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday","Thursday", "Friday", "Saturday"}
    
print(days[1])     --> Sunday
days[2] = "Noday"
stdin:1: attempt to update a read-only table

最后講一下這個(gè) __call 元方法

> a = {} 
> mt = {}
> mt.__call = function(...) print("call mt.__call",...) end --定義我們的元方法
> a(1,2,3) --因?yàn)槲覀兊腶是一個(gè)table,所以直接這樣調(diào)用是不能的哪廓。
stdin:1: attempt to call a table value (global 'a')
stack traceback:
        stdin:1: in main chunk
        [C]: in ?


> setmetatable(a,mt) --這里設(shè)置一下元表
table: 00bdcce0
> a(4,5,6)
call mt.__call  table: 00bdcce0 4       5       6
> mt
table: 00bdcc18
> a
table: 00bdcce0
>

__call在調(diào)用的時(shí)候狗唉,默認(rèn)會(huì)把對(duì)象作為第一個(gè)參數(shù)也傳入進(jìn)去,上面的例子就是把a(bǔ)也傳入進(jìn)去涡真。所以我們定義__call的時(shí)候可以寫成下面這樣:

mt.__call = function(ins,...) print("ins=",ins,...) end
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末分俯,一起剝皮案震驚了整個(gè)濱河市肾筐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌缸剪,老刑警劉巖吗铐,帶你破解...
    沈念sama閱讀 222,627評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異杏节,居然都是意外死亡唬渗,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門奋渔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)镊逝,“玉大人,你說(shuō)我怎么就攤上這事嫉鲸〕潘猓” “怎么了?”我有些...
    開封第一講書人閱讀 169,346評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵玄渗,是天一觀的道長(zhǎng)座菠。 經(jīng)常有香客問(wèn)我,道長(zhǎng)藤树,這世上最難降的妖魔是什么辈灼? 我笑而不...
    開封第一講書人閱讀 60,097評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮也榄,結(jié)果婚禮上巡莹,老公的妹妹穿的比我還像新娘。我一直安慰自己甜紫,他們只是感情好降宅,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著囚霸,像睡著了一般腰根。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拓型,一...
    開封第一講書人閱讀 52,696評(píng)論 1 312
  • 那天额嘿,我揣著相機(jī)與錄音,去河邊找鬼劣挫。 笑死册养,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的压固。 我是一名探鬼主播球拦,決...
    沈念sama閱讀 41,165評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了坎炼?” 一聲冷哼從身側(cè)響起愧膀,我...
    開封第一講書人閱讀 40,108評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谣光,沒(méi)想到半個(gè)月后檩淋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,646評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萄金,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評(píng)論 3 342
  • 正文 我和宋清朗相戀三年狼钮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捡絮。...
    茶點(diǎn)故事閱讀 40,861評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡熬芜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出福稳,到底是詐尸還是另有隱情涎拉,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布的圆,位于F島的核電站鼓拧,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏越妈。R本人自食惡果不足惜季俩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梅掠。 院中可真熱鬧酌住,春花似錦、人聲如沸阎抒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)且叁。三九已至都哭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間逞带,已是汗流浹背欺矫。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留展氓,地道東北人司忱。 一個(gè)月前我還...
    沈念sama閱讀 49,287評(píng)論 3 379
  • 正文 我出身青樓塑煎,卻偏偏與公主長(zhǎng)得像沈自,于是被迫代替她去往敵國(guó)和親劫拗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子阅羹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評(píng)論 2 361

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

  • —寫在抗戰(zhàn)勝利日 公歷8月15日勺疼。 一個(gè)平常的日子教寂,陽(yáng)光像昨日一樣燦爛;清晨执庐、午后和傍晚像往日一樣靜靜地降臨酪耕、又靜...
    W和W閱讀 312評(píng)論 1 4
  • 聽網(wǎng)課迂烁,發(fā)覺(jué)最重要的是有水平。表達(dá)方式递鹉、營(yíng)造氛圍都是錦上添花的東西盟步。所以也要發(fā)覺(jué)自己最有水平的一面啊~
    胡涂格格閱讀 128評(píng)論 0 0