補(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ù)getmetatable
和setmetatable
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)算遵循:
- 如果第一個(gè)值有元表洞翩,并且有對(duì)應(yīng)的操作符重載稽犁,lua選擇它作為這次運(yùn)算的方法。不依賴第二個(gè)值
- 否則骚亿,如果第二個(gè)值有元表已亥,并且有對(duì)應(yīng)的操作符重載,lua選擇它作為這次運(yùn)算的方法来屠。
- 否則虑椎,lua raises an error(報(bào)錯(cuò))
按照上面的規(guī)則,1+s1
和s1+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