Lua中的元表與元方法
前言
Lua中每一個值都可具有元表。 元表是普通的Lua表,定義了原始值在某些特定操作下的行為。
你可通過在值的原表中設置特定的字段來改變作用於該值的操作的某些行為特征。
比如。當數字值作為加法的操作數時,Lua檢查其元表中的"__add"字段是否有個函數。假設有,Lua調用它運行加法。
我們稱元表中的鍵為事件(event)。稱值為元方法(metamethod)。前述樣例中的事件是"add",元方法是運行加法的函數。
可通過函數getmetatable查詢不論什麽值的元表。
在table中,我能夠又一次定義的元方法有下面幾個:
__add(a, b) --加法 __sub(a, b) --減法 __mul(a, b) --乘法 __div(a, b) --除法 __mod(a, b) --取模 __pow(a, b) --乘冪 __unm(a) --相反數 __concat(a, b) --連接 __len(a) --長度 __eq(a, b) --相等 __lt(a, b) --小於 __le(a, b) --小於等於 __index(a, b) --索引查詢 __newindex(a, b, c) --索引更新(PS:不懂的話,後面會有講) __call(a, ...) --運行方法調用 __tostring(a) --字符串輸出 __metatable --保護元表
Lua中的每一個表都有其Metatable。Lua默認創建一個不帶metatable的新表
t = {} print(getmetatable(t)) --> nil能夠使用setmetatable函數設置或者改變一個表的metatable
t1 = {} setmetatable(t, t1) assert(getmetatable(t) == t1)
不論什麽一個表都能夠是其它一個表的metatable,一組相關的表能夠共享一個metatable(描寫敘述他們共同的行為)。一個表也能夠是自身的metatable(描寫敘述其私有行為)。
接下來就介紹介紹假設去又一次定義這些方法。
算術類的元方法
如今我使用完整的實例代碼來具體的說明算術類元方法的使用。
Set = {} local mt = {} -- 集合的元表 -- 依據參數列表中的值創建一個新的集合 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{} -- 此處相當於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
代碼例如以下:
Set = {} local mt = {} -- 集合的元表 -- 依據參數列表中的值創建一個新的集合 function Set.new(l) local set = {} setmetatable(set, mt) for _, v in pairs(l) do set[v] = true end return set end在此之後,全部由Set.new創建的集合都具有一個同樣的元表。比如:
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這以後,僅僅要我們使用“+”符號求兩個集合的並集,它就會自己主動的調用Set.union函數,並將兩個操作數作為參數傳入。比方下面代碼:
local set1 = Set.new({10, 20, 30}) local set2 = Set.new({1, 2}) local set3 = set1 + set2 Set.print(set3)在上面列舉的那些能夠重定義的元方法都能夠使用上面的方法進行重定義。如今就出現了一個新的問題,set1和set2都有元表。那我們要用誰的元表啊?盡管我們這裏的演示樣例代碼使用的都是一個元表。可是實際coding中,會遇到我這裏說的問題,對於這種問題。Lua是依照下面步驟進行解決的:
- 對於二元操作符。假設第一個操作數有元表,而且元表中有所須要的字段定義。比方我們這裏的__add元方法定義。那麽Lua就以這個字段為元方法,而與第二個值無關;
- 對於二元操作符,假設第一個操作數有元表,可是元表中沒有所須要的字段定義,比方我們這裏的__add元方法定義。那麽Lua就去查找第二個操作數的元表;
- 假設兩個操作數都沒有元表。或者都沒有相應的元方法定義。Lua就引發一個錯誤。
比方set3 = set1 + 8這種代碼。就會打印出下面的錯誤提示:
lua: test.lua:16: bad argument #1 to ‘pairs‘ (table expected, got number)可是,我們在實際編碼中。能夠依照下面方法,彈出我們定義的錯誤消息,代碼例如以下:
function Set.union(a, b) if getmetatable(a) ~= mt or getmetatable(b) ~= mt then error("metatable error.") end local retSet = Set.new{} -- 此處相當於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當兩個操作數的元表不是同一個元表時。就表示二者進行並集操作時就會出現故障,那麽我們就能夠打印出我們須要的錯誤消息。
上面總結了算術類的元方法的定義。關系類的元方法和算術類的元方法的定義是相似的,這裏不做累述。
__tostring元方法
寫過Java或者C#的人都知道。Object類中都有一個tostring的方法,程序猿能夠重寫該方法。以實現自己的需求。
在Lua中。也是這種。當我們直接print(a)(a是一個table)時,是不能夠的。那怎麽辦。這個時候,我們就須要自己又一次定義__tostring元方法,讓print能夠格式化打印出table類型的數據。
函數print總是調用tostring來進行格式化輸出。當格式化隨意值時。tostring會檢查該值是否有一個__tostring的元方法,假設有這個元方法,tostring就用該值作為參數來調用這個元方法,剩下實際的格式化操作就由__tostring元方法引用的函數去完畢,該函數終於返回一個格式化完畢的字符串。
比例如以下面代碼:
mt.__tostring = Set.toString
怎樣保護我們的“奶酪”——元表
我們會發現。使用getmetatable就能夠非常輕易的得到元表,使用setmetatable就能夠非常easy的改動元表,那這樣做的風險是不是太大了。那麽怎樣保護我們的元表不被篡改呢?在Lua中,函數setmetatable和getmetatable函數會用到元表中的一個字段,用於保護元表,該字段是__metatable。當我們想要保護集合的元表,是用戶既不能看也不能改動集合的元表,那麽就須要使用__metatable字段了;當設置了該字段時,getmetatable就會返回這個字段的值,而setmetatable則會引發一個錯誤;例如以下面演示代碼:
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" -- 設置完我的元表以後。不讓其它人再設置 return set end local tb = Set.new({1, 2}) print(tb) print(getmetatable(tb)) setmetatable(tb, {})上述代碼就會打印下面內容:
{1, 2} You cannot get the metatable lua: test.lua:56: cannot change a protected metatable
__index元方法
是否還記得當我們訪問一個table中不存在的字段時,會返回什麽值?默認情況下。當我們訪問一個table中不存在的字段時,得到的結果是nil。
可是這種狀況非常easy被改變。Lua是依照下面的步驟決定是返回nil還是其它值得:
- 當訪問一個table的字段時,假設table有這個字段。則直接返回相應的值;
- 當table沒有這個字段。則會促使解釋器去查找一個叫__index的元方法。接下來就就會調用相應的元方法。返回元方法返回的值。
- 假設沒有這個元方法,那麽就返回nil結果。
下面通過一個實際的樣例來說明__index的使用。
假設要創建一些描寫敘述窗體,每一個table中都必須描寫敘述一些窗體參數,比如顏色,位置和大小等,這些參數都是有默認值得。因此。我們在創建窗體對象時能夠指定那些不同於默認值得參數。
Windows = {} -- 創建一個命名空間 -- 創建默認值表 Windows.default = {x = 0, y = 0, width = 100, height = 100, color = {r = 255, g = 255, b = 255}} Windows.mt = {} -- 創建元表 -- 聲明構造函數 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 訪問自身已經擁有的值 print(win.width) -- >100 訪問default表中的值 print(win.color.r) -- >255 訪問default表中的值
依據上面代碼的輸出。結合上面說的那三步,我們再來看看,print(win.x)時。因為win變量本身就擁有x字段。所以就直接打印了其自身擁有的字段的值。print(win.width),因為win變量本身沒有width字段,那麽就去查找是否擁有元表,元表中是否有__index相應的元方法。因為存在__index元方法,返回了default表中的width字段的值,print(win.color.r)也是同樣的道理。
在實際編程中,__index元方法不必一定是一個函數,它還能夠是一個table。當它是一個函數時,Lua以table和不存在key作為參數來調用該函數,這就和上面的代碼一樣;當它是一個table時,Lua就以同樣的方式來又一次訪問這個table,所以上面的代碼也能夠是這種:
-- 定義__index元方法 Windows.mt.__index = Windows.default
__newindex元方法
__newindex元方法與__index相似。__newindex用於更新table中的數據,而__index用於查詢table中的數據。當對一個table中不存在的索引賦值時,在Lua中是依照下面步驟進行的:
Lua解釋器先推斷這個table是否有元表。
- 假設有了元表,就查找元表中是否有__newindex元方法。假設沒有元表,就直接加入這個索引。然後相應的賦值;
- 假設有這個__newindex元方法,Lua解釋器就運行它。而不是運行賦值。
- 假設這個__newindex相應的不是一個函數。而是一個table時,Lua解釋器就在這個table中運行賦值。而不是對原來的table。
那麽這裏就出現了一個問題。看下面代碼:
local tb1 = {} local tb2 = {} tb1.__newindex = tb2 tb2.__newindex = tb1 setmetatable(tb1, tb2) setmetatable(tb2, tb1) tb1.x = 10
發現什麽問題了麽?是不是循環了,在Lua解釋器中,對這個問題,就會彈出錯誤消息,錯誤消息例如以下:
loop in settable
引用博客:http://www.jellythink.com/archives/511
Lua中的元表與元方法