Lua中每個(gè)值都可具有元表。 元表是普通的Lua表,定義了原始值在某些特定操作下的行為。你可通過在值的原表中設(shè)置特定的字段來改變作用于該值的操作的某些行為特征。
例如,當(dāng)一個(gè)非數(shù)字值作為加法的操作數(shù)時(shí),Lua檢查其元表中的"__add"字段是否有個(gè)函數(shù)。如果有,Lua調(diào)用它執(zhí)行加法。
我們稱元表中的鍵為事件(event),稱值為元方法(metamethod)。前述例子中的事件是"add",元方法是執(zhí)行加法的函數(shù)。
可通過函數(shù)getmetatable查詢?nèi)魏沃档脑怼?/p>
在table中,我可以重新定義的元方法有以下幾個(gè):
__add(a, b) --加法
__sub(a, b) --減法
__mul(a, b) --乘法
__div(a, b) --除法
__mod(a, b) --取模
__pow(a, b) --乘冪
__unm(a) --相反數(shù)
__concat(a, b) --連接
__len(a) --長(zhǎng)度
__eq(a, b) --相等
__lt(a, b) --小于
__le(a, b) --小于等于
__index(a, b) --索引查詢
__newindex(a, b, c) --索引更新(PS:不懂的話,后面會(huì)有講)
__call(a, ...) --執(zhí)行方法調(diào)用
__tostring(a) --字符串輸出
__metatable --保護(hù)元表
Lua中的每一個(gè)表都有其Metatable。Lua默認(rèn)創(chuàng)建一個(gè)不帶metatable的新表
t = {}
print(getmetatable(t)) --> nil
可以使用setmetatable函數(shù)設(shè)置或者改變一個(gè)表的metatable
t1 = {}
setmetatable(t, t1)
assert(getmetatable(t) == t1)
任何一個(gè)表都可以是其他一個(gè)表的metatable,一組相關(guān)的表可以共享一個(gè)metatable(描述他們共同的行為)。一個(gè)表也可以是自身的metatable(描述其私有行為)。
接下來就介紹介紹如果去重新定義這些方法。
現(xiàn)在我使用完整的實(shí)例代碼來詳細(xì)的說明算術(shù)類元方法的使用。
Set = {}
local mt = {} -- 集合的元表
-- 根據(jù)參數(shù)列表中的值創(chuàng)建一個(gè)新的集合
function Set.new(l)
local set = {}
setmetatable(set, mt)
for _, v in pairs(l) do set[v] = true end
return set
end
-- 并集操作
function Set.union(a, b)
local retSet = Set.new{} -- 此處相當(dāng)于Set.new({})
for v in pairs(a) do retSet[v] = true end
for v in pairs(b) do retSet[v] = true end
return retSet
end
-- 交集操作
function Set.intersection(a, b)
local retSet = Set.new{}
for v in pairs(a) do retSet[v] = b[v] end
return retSet
end
-- 打印集合的操作
function Set.toString(set)
local tb = {}
for e in pairs(set) do
tb[#tb + 1] = e
end
return "{" .. table.concat(tb, ", ") .. "}"
end
function Set.print(s)
print(Set.toString(s))
end
現(xiàn)在,我定義“+”來計(jì)算兩個(gè)集合的并集,那么就需要讓所有用于表示集合的table共享一個(gè)元表,并且在該元表中定義如何執(zhí)行一個(gè)加法操作。首先創(chuàng)建一個(gè)常規(guī)的table,準(zhǔn)備用作集合的元表,然后修改Set.new函數(shù),在每次創(chuàng)建集合的時(shí)候,都為新的集合設(shè)置一個(gè)元表。代碼如下:
Set = {}
local mt = {} -- 集合的元表
-- 根據(jù)參數(shù)列表中的值創(chuàng)建一個(gè)新的集合
function Set.new(l)
local set = {}
setmetatable(set, mt)
for _, v in pairs(l) do set[v] = true end
return set
end
在此之后,所有由Set.new創(chuàng)建的集合都具有一個(gè)相同的元表,例如:
local set1 = Set.new({10, 20, 30})
local set2 = Set.new({1, 2})
print(getmetatable(set1))
print(getmetatable(set2))
assert(getmetatable(set1) == getmetatable(set2))
最后,我們需要把元方法加入元表中,代碼如下:
mt.__add = Set.union
這以后,只要我們使用“+”符號(hào)求兩個(gè)集合的并集,它就會(huì)自動(dòng)的調(diào)用Set.union函數(shù),并將兩個(gè)操作數(shù)作為參數(shù)傳入。比如以下代碼:
local set1 = Set.new({10, 20, 30})
local set2 = Set.new({1, 2})
local set3 = set1 + set2
Set.print(set3)
在上面列舉的那些可以重定義的元方法都可以使用上面的方法進(jìn)行重定義?,F(xiàn)在就出現(xiàn)了一個(gè)新的問題,set1和set2都有元表,那我們要用誰的元表啊?雖然我們這里的示例代碼使用的都是一個(gè)元表,但是實(shí)際coding中,會(huì)遇到我這里說的問題,對(duì)于這種問題,Lua是按照以下步驟進(jìn)行解決的:
以上就是Lua處理這個(gè)問題的規(guī)則,那么我們?cè)趯?shí)際編程中該如何做呢?
比如set3 = set1 + 8這樣的代碼,就會(huì)打印出以下的錯(cuò)誤提示:
lua: test.lua:16: bad argument #1 to 'pairs' (table expected, got number)
但是,我們?cè)趯?shí)際編碼中,可以按照以下方法,彈出我們定義的錯(cuò)誤消息,代碼如下:
function Set.union(a, b)
if getmetatable(a) ~= mt or getmetatable(b) ~= mt then
error("metatable error.")
end
local retSet = Set.new{} -- 此處相當(dāng)于Set.new({})
for v in pairs(a) do retSet[v] = true end
for v in pairs(b) do retSet[v] = true end
return retSet
end
當(dāng)兩個(gè)操作數(shù)的元表不是同一個(gè)元表時(shí),就表示二者進(jìn)行并集操作時(shí)就會(huì)出現(xiàn)問題,那么我們就可以打印出我們需要的錯(cuò)誤消息。
上面總結(jié)了算術(shù)類的元方法的定義,關(guān)系類的元方法和算術(shù)類的元方法的定義是類似的,這里不做累述。
寫過Java或者C#的人都知道,Object類中都有一個(gè)tostring的方法,程序員可以重寫該方法,以實(shí)現(xiàn)自己的需求。在Lua中,也是這樣的,當(dāng)我們直接print(a)(a是一個(gè)table)時(shí),是不可以的。那怎么辦,這個(gè)時(shí)候,我們就需要自己重新定義tostring元方法,讓print可以格式化打印出table類型的數(shù)據(jù)。
函數(shù)print總是調(diào)用tostring來進(jìn)行格式化輸出,當(dāng)格式化任意值時(shí),tostring會(huì)檢查該值是否有一個(gè)tostring的元方法,如果有這個(gè)元方法,tostring就用該值作為參數(shù)來調(diào)用這個(gè)元方法,剩下實(shí)際的格式化操作就由__tostring元方法引用的函數(shù)去完成,該函數(shù)最終返回一個(gè)格式化完成的字符串。例如以下代碼:
mt.__tostring = Set.toString
我們會(huì)發(fā)現(xiàn),使用getmetatable就可以很輕易的得到元表,使用setmetatable就可以很容易的修改元表,那這樣做的風(fēng)險(xiǎn)是不是太大了,那么如何保護(hù)我們的元表不被篡改呢?在Lua中,函數(shù)setmetatable和getmetatable函數(shù)會(huì)用到元表中的一個(gè)字段,用于保護(hù)元表,該字段是metatable。當(dāng)我們想要保護(hù)集合的元表,是用戶既不能看也不能修改集合的元表,那么就需要使用metatable字段了;當(dāng)設(shè)置了該字段時(shí),getmetatable就會(huì)返回這個(gè)字段的值,而setmetatable則會(huì)引發(fā)一個(gè)錯(cuò)誤;如以下演示代碼:
function Set.new(l)
local set = {}
setmetatable(set, mt)
for _, v in pairs(l) do set[v] = true end
mt.__metatable = "You cannot get the metatable" -- 設(shè)置完我的元表以后,不讓其他人再設(shè)置
return set
end
local tb = Set.new({1, 2})
print(tb)
print(getmetatable(tb))
setmetatable(tb, {})
上述代碼就會(huì)打印以下內(nèi)容:
{1, 2}
You cannot get the metatable
lua: test.lua:56: cannot change a protected metatable
是否還記得當(dāng)我們?cè)L問一個(gè)table中不存在的字段時(shí),會(huì)返回什么值?默認(rèn)情況下,當(dāng)我們?cè)L問一個(gè)table中不存在的字段時(shí),得到的結(jié)果是nil。但是這種狀況很容易被改變;Lua是按照以下的步驟決定是返回nil還是其它值得:
下面通過一個(gè)實(shí)際的例子來說明__index的使用。假設(shè)要?jiǎng)?chuàng)建一些描述窗口,每個(gè)table中都必須描述一些窗口參數(shù),例如顏色,位置和大小等,這些參數(shù)都是有默認(rèn)值得,因此,我們?cè)趧?chuàng)建窗口對(duì)象時(shí)可以指定那些不同于默認(rèn)值得參數(shù)。
Windows = {} -- 創(chuàng)建一個(gè)命名空間
-- 創(chuàng)建默認(rèn)值表
Windows.default = {x = 0, y = 0, width = 100, height = 100, color = {r = 255, g = 255, b = 255}}
Windows.mt = {} -- 創(chuàng)建元表
-- 聲明構(gòu)造函數(shù)
function Windows.new(o)
setmetatable(o, Windows.mt)
return o
end
-- 定義__index元方法
Windows.mt.__index = function (table, key)
return Windows.default[key]
end
local win = Windows.new({x = 10, y = 10})
print(win.x) -- >10 訪問自身已經(jīng)擁有的值
print(win.width) -- >100 訪問default表中的值
print(win.color.r) -- >255 訪問default表中的值
根據(jù)上面代碼的輸出,結(jié)合上面說的那三步,我們?cè)賮砜纯?,print(win.x)時(shí),由于win變量本身就擁有x字段,所以就直接打印了其自身?yè)碛械淖侄蔚闹担籶rint(win.width),由于win變量本身沒有width字段,那么就去查找是否擁有元表,元表中是否有index對(duì)應(yīng)的元方法,由于存在index元方法,返回了default表中的width字段的值,print(win.color.r)也是同樣的道理。
在實(shí)際編程中,__index元方法不必一定是一個(gè)函數(shù),它還可以是一個(gè)table。當(dāng)它是一個(gè)函數(shù)時(shí),Lua以table和不存在key作為參數(shù)來調(diào)用該函數(shù),這就和上面的代碼一樣;當(dāng)它是一個(gè)table時(shí),Lua就以相同的方式來重新訪問這個(gè)table,所以上面的代碼也可以是這樣的:
-- 定義__index元方法
Windows.mt.__index = Windows.default
newindex元方法與index類似,newindex用于更新table中的數(shù)據(jù),而index用于查詢table中的數(shù)據(jù)。當(dāng)對(duì)一個(gè)table中不存在的索引賦值時(shí),在Lua中是按照以下步驟進(jìn)行的:
Lua解釋器先判斷這個(gè)table是否有元表;
那么這里就出現(xiàn)了一個(gè)問題,看以下代碼:
local tb1 = {}
local tb2 = {}
tb1.__newindex = tb2
tb2.__newindex = tb1
setmetatable(tb1, tb2)
setmetatable(tb2, tb1)
tb1.x = 10
發(fā)現(xiàn)什么問題了么?是不是循環(huán)了,在Lua解釋器中,對(duì)這個(gè)問題,就會(huì)彈出錯(cuò)誤消息,錯(cuò)誤消息如下:
loop in settable
更多建議: