lua(6)-元表(metatable)和元方法(meatmethod)
通常,Lua中的每個值都有一套預定義的操作集合。例如,可以將數字相加,可以連線字串。但是我們無法將兩個table相加,無法對函式作比較,也無法呼叫一個字串。因此可以通過元表來修改一個值的行為,使其在面對一個非預定義的操作時執行一個指定的操作。例如,假設a和b都是table,通過元表可以定義如何計算表示式a+b。當Lua試圖將兩個table相加時,它會先檢查兩者之一是否有元表,然後檢查該元表中是否有一個叫__add的欄位。如果Lua找到了該欄位,就呼叫該欄位對應的值。這個值也就是“元方法”,它應該是一個函式。
簡單地說,元表和元方法就類似於C++、Java等語言中的操作符過載。
預設情況下,除了table和userdata型別,其他的型別預設共享一個元表,因此可以實現諸如以下的這些操作
local a = 1
local b = 2
c = a+b
或
local d ="s1"
local e ="s2"
local f = d..e
在lua程式碼中宣告和定義一個變數的時候,不會為這個變數建立一個新的元表,因此如果想實現兩個table相加或相減等操作,得為這兩個table設定一個元表,同時賦予相加操作的元方法。例如,過載"+"操作符,使其能允許兩個table相加,則需重寫元表中的__add方法,重寫了__add元方法後,當使用”+”操作符時,如果運算元經過tonumber為nil,如果不為nil,則呼叫原生的”+”號方法,如果為nil,則檢查兩個運算元中是否含有__add方法,如果有,則執行__add方法,否則丟擲異常。
__add | 符號"+"的元方法(二元操作符,相加) |
__sub | 符號"-"的元方法 (二元操作符,相減) |
__mul | 符號"*"的元方法 (二元操作符,相乘) |
__div | 符號"/"的元方法 (二元操作符,相除) |
__mod | 符號"%"的元方法 (二元操作符,取餘) |
__pow | 符號"^"的元方法 (二元操作符,次冪) |
__unm | 符號"-"的元方法(一元操作符,正負取反) |
__concat | 符號".."的元方法 (二元操作符,連線) |
__len | 符號"#"的元方法(一元操作符,取長度) |
__eq | 符號"=="的元方法 (二元操作符,是否相等) |
__lt | 符號"<"或">"的元方法 (二元操作符,小於) |
__le | 符號"<="或">="的元方法 (二元操作符,小於等於) |
__index | table元素的下標 |
__newindex | table新分配元素的下標 |
__call | 呼叫一個變數 |
__tostring | 格式化為字串 |
__metatable | 元表 |
__mode | 引用模式(用於設定一個值是否是弱引用) |
(1)__add元方法。
__add元方法對應符號”+”。對於相加的兩個運算元op1與op2,op1+op2會進行如下操作:
function add_event (op1, op2)
local o1,o2 = tonumber(op1), tonumber(op2)
if o1 ando2 then -- (是否op1和op2都可轉為數字?)
returno1 + o2 -- (這裡的"+"代表原始的加法)
else -- (如果有一個及以上的運算元不能轉化為數字)
--(獲取運算元的__add元方法)
localh = getbinhandler(op1, op2, "__add")
ifh then
--(呼叫這個__add元方法,並將兩個運算元傳遞給這個元方法)
return(h(op1, op2))
else -- (如果沒有元方法,丟擲異常)
error(···)
end
end
end
以下示例過載__add方法實現兩個table相加。
輸出
(2)__sub元方法。
__sub元方法對應符號”-”。
以下示例過載__sub方法實現兩個table相減。
輸出
__mul、__div、__mod等算術類元方法的實現與(1)和(2)相似。
(3)__unm元方法。
__unm元方法是lua中正負數取反的元方法,對應一元操作符”-”,呼叫__unm元方法的邏輯原型如下:
function unm_event (op)
local o =tonumber(op)
if othen -- (操作符是否是數字?)
return-o -- (取反返回,這裡的"-"是原始的取反方法)
else -- (如果運算元不是數字)
-- (獲取__unm元方法)
localh = metatable(op).__unm
ifh then
--(呼叫__unm元方法)
return(h(op))
else -- (丟擲異常)
error(···)
end
end
end
以下示例過載__unm方法實現對一個table取反。
輸出
(4)__eq元方法。
__eq是lua中關係型的元方法,用於判斷兩個運算元是否相等,即”==”。lua提供的元方法欄位中並沒有"~="符號的元方法,因為lua程式碼被編譯時將"~="轉化成了not(a==b);同樣的,a>b會轉化為b<a,a>=b轉化為b<=a。
關係型的元方法也會對傳入元方法的多個元素進行自己的操作。
__eq示例判斷兩個table是否相等。
輸出
(5)__tostring元方法。
以上的元方法都是基於操作符號,lua中還有基於關鍵字的元方法,這種元方法叫做庫定義元方法。比如我們使用print(value)關鍵字的時候,總能將value的型別格式化成符合print關鍵字語法的型別,print關鍵字在使用時會在value的元表中查詢__tostring元方法,類似於__tostring就是庫定義元方法。
我們來對table進行庫定義方法的修改
__tostring示例列印一個table。
輸出
(6)__concat元方法。
__concat元方法是lua中字串連線的方法,對應二元操作符”..”,呼叫__concat元方法的邏輯原型如下:
function concat_event (op1, op2)
if(type(op1) == "string" or type(op1) == "number") and
(type(op2)== "string" or type(op2) == "number") then
returnop1 .. op2 -- (這裡的".."是原始的字串連線操作)
else
localh = getbinhandler(op1, op2, "__concat")
ifh then
return(h(op1, op2))
else
error(···)
end
end
end
以下示例過載__concat元方法實現連線兩個table的字串。
輸出
(7)__len元方法。
__len元方法是lua中取操作符長度的方法,對應一元操作符”#”,呼叫__len元方法的邏輯原型如下:
function len_event (op)
if type(op) == "string" then
return strlen(op) -- (原始的取字串長度)
elseif type(op) == "table" then
return #op -- (原始的取table長度)
else
local h = metatable(op).__len
if h then
return (h(op))
else
error(···)
end
end
end
(8)__lt元方法。
__lt元方法是lua中“小於”的方法,對應一元操作符”<”,呼叫__lt元方法的邏輯原型如下:
function lt_event (op1, op2)
if type(op1) == "number" and type(op2) =="number" then
return op1 < op2 -- (對數字進行原始的"<"操作)
elseif type(op1) == "string" and type(op2) =="string" then
return op1 < op2 -- (對字串進行原始的"<操作")
else
local h = getcomphandler(op1, op2, "__lt")
if h then
return (h(op1, op2))
else
error(···)
end
end
end
以下示例過載__lt元方法實現兩個數字table的大小比較。
輸出
(9)__le元方法。
__le元方法是lua中“小於等於”的方法,對應一元操作符”<=”,其函式原型與重寫方法與__lt類似。
(10)__index元方法。
__index元方法用於索引一個table的元素。對於table[key],如果table[key]為nil,則會呼叫__index元方法,如果__index返回的值為nin,table[key]才會返回nil。
呼叫__index元方法的邏輯原型如下:
function gettable_event (table, key)
local h
iftype(table) == "table" then
localv = rawget(table, key) -- (用原始的方法獲得table[key]值)
ifv ~= nil then return v end
h =metatable(table).__index --(如果table[key]為空,呼叫__index)
ifh == nil then return nil end
else
h =metatable(table).__index
ifh == nil then
error(···)
end
end
if type(h)== "function" then
return(h(table, key)) -- (如果__index是function型別,呼叫它)
elsereturn h[key] -- (如果__index是table型別,將索引__index[key]的值)
end
end
__index元方法可以是函式,也可以是一個table。以下示例一個面向物件的例子,這裡__index是一個函式,假設有People這個類,People裡面含有name、age這些變數,含有SayHello、New這些成員方法,建立People的兩個例項物件XiaoMing和LiHua,然後呼叫這兩個例項物件中不存在的name、age、SayHello、New等欄位。
輸出
當__index元方法是一個table的時候,table[key]如果為nil,則會去查詢__index[key]。以下示例當__index是一個table的時候的用法。
輸出
如果在訪問一個table時,不想觸發它的__index元方法,可以使用函式rawget(t,i)來獲取table中元素的值。
輸出
(11)__newindex元方法。
__newindex元方法與__index元方法類似,只是__index用於索引一個table的值,而__newindex用於table的賦值,也就是table[key] = value的時候,如果table[key]不存在,則會查詢__newindex元方法,如果__newindex不為nil,則呼叫__newindex元方法。
呼叫__newindex元方法的邏輯原型如下:
function settable_event (table, key, value)
local h
iftype(table) == "table" then
localv = rawget(table, key)
ifv ~= nil then rawset(table, key, value); return end
h =metatable(table).__newindex
ifh == nil then rawset(table, key, value); return end
else
h =metatable(table).__newindex
ifh == nil then
error(···)
end
end
if type(h)== "function" then
h(table,key,value) -- (如果__newindex是function,呼叫它)
elseh[key] = value -- (如果__newindex是table,索引__newindex[key])
end
end
以下示例當__newindex為函式時的用法。
輸出
(12)__call元方法。
__call元方法是“呼叫”的方法,對應符號”()”,當在一個物件的後面使用呼叫,lua首先會判斷這個物件是否是function型別,如果是則按照執行函式的形式執行這個物件;如果這個物件不是function型別,則呼叫__call方法。
呼叫__call元方法的邏輯原型如下:
function function_event (func, ...)
iftype(func) == "function" then
returnfunc(...) -- (原始的呼叫)
else
localh = metatable(func).__call
ifh then
returnh(func, ...)
else
error(···)
end
end
end
以下示例__call元方法的用法,實現當呼叫一個table時,列印它的引數。
輸出
(13)__mode元方法。
__mode是lua用於實現table的弱引用的元方法(可以參考lua(5)-table(表)這篇文章),每一次lua的記憶體回收都會檢測該table內是否含有__mode元方法,當一個table的__mode元方法被宣告並定義後,記憶體回收將會清除table被標記為“垃圾”的物件。
以下示例__mode元方法的使用
輸出