1. 程式人生 > >lua(6)-元表(metatable)和元方法(meatmethod)

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,lua還提供了其餘操作符號的元方法關鍵字,包括關係類元方法和庫定義的元方法。

常用的元方法欄位
__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元方法的使用


輸出