1. 程式人生 > >熟練使用Lua(二)語言核心:table及Metatable、MetaMethod操作

熟練使用Lua(二)語言核心:table及Metatable、MetaMethod操作

table

table 型別實現了一個關聯陣列。也就是說,陣列可以用任何東西(除了nil)做索引,而不限於數字。 table 可以以不同型別的值構成;它可以包含所有的型別的值(除 nil 外)。 table 是 lua 中唯一的一種資料結構;它可以用來描述原始的陣列、符號表、集合、記錄、圖、樹、等等。用於表述記錄時,lua 使用域名作為索引。語言本身採用一種語法糖,支援以 a.name 的形式表示 a[“name”]。有很多形式用於在 lua 中建立一個 table。

跟索引一樣, table 每個域中的值也可以是任何型別(除 nil外)。特別的,因為函式本身也是值,所以 table 的域中也可以放函式。這樣 table 中就可以有一些 methods 了。

**

Metatable(元表)

**
Lua 中的每個值都可以用一個 metatable。這個 metatable 就是一個原始的 Lua table ,它用來定義原始值在特定操作下的行為。你可以通過在 metatable 中的特定域設一些值來改變擁有這個 metatable 的值的指定操作之行為。舉例來說,當一個非數字的值作加法操作的時候, Lua 會檢查它的 metatable 中 “__add” 域中的是否有一個函式。如果有這麼一個函式的話,Lua 呼叫這個函式來執行一次加法。

我們叫 metatable 中的鍵名為 事件 (event) ,把其中的值叫作 元方法 (metamethod)。在上個例子中,事件是 “add” 而元方法就是那個執行加法操作的函式。

你可以通過 getmetatable 函式來查詢到任何一個值的 metatable。

你可以通過 setmetatable 函式來替換掉 table 的 metatable 。你不能從 Lua 中改變其它任何型別的值的 metatable (使用 debug 庫例外);要這樣做的話必須使用 C API 。

每個 table 和 userdata 擁有獨立的 metatable (當然多個 table 和 userdata 可以共享一個相同的表作它們的 metatable);其它所有型別的值,每種型別都分別共享唯一的一個 metatable。因此,所有的數字一起只有一個 metatable ,所有的字串也是,等等。

一個 metatable 可以控制一個物件做數學運算操作、比較操作、連線操作、取長度操作、取下標操作時的行為, metatable 中還可以定義一個函式,讓 userdata 作垃圾收集時呼叫它。對於這些操作,Lua 都將其關聯上一個被稱作事件的指定健。當 Lua 需要對一個值發起這些操作中的一個時,它會去檢查值中 metatable 中是否有對應事件。如果有的話,鍵名對應的值(元方法)將控制 Lua 怎樣做這個操作。

metatable 可以控制的操作已在下面列出來。每個操作都用相應的名字區分。每個操作的鍵名都是用操作名字加上兩個下劃線 ‘__’ 字首的字串;舉例來說,“add” 操作的鍵名就是字串 “__add”。這些操作的語義用一個 Lua 函式來描述直譯器如何執行更為恰當。

這裡展示的用 Lua 寫的程式碼僅作解說用;實際的行為已經硬編碼在直譯器中,其執行效率要遠高於這些模擬程式碼。這些用於描述的的程式碼中用到的函式( rawget , tonumber ,等等。)都可以在 §5.1 中找到。特別注意,我們使用這樣一個表示式來從給定物件中提取元方法

metatable(obj)[event]

這個應該被解讀作

 rawget(getmetatable(obj) or {}, event)

這就是說,訪問一個元方法不再會觸發任何的元方法,而且訪問一個沒有 metatable 的物件也不會失敗(而只是簡單返回 nil)。

“add”: + 操作。
下面這個 getbinhandler 函式定義了 Lua 怎樣選擇一個處理器來作二元操作。首先,Lua 嘗試第一個運算元。如果這個東西的型別沒有定義這個操作的處理器,然後 Lua 會嘗試第二個運算元。

 function getbinhandler (op1, op2, event)
   return metatable(op1)[event] or metatable(op2)[event]
 end

通過這個函式, op1 + op2 的行為就是

 function add_event (op1, op2)
   local o1, o2 = tonumber(op1), tonumber(op2)
   if o1 and o2 then  -- 兩個運算元都是數字?
     return o1 + o2   -- 這裡的 '+' 是原生的 'add'
   else  -- 至少一個運算元不是數字時
     local h = getbinhandler(op1, op2, "__add")
     if h then
       -- 以兩個運算元來呼叫處理器
       return h(op1, op2)
     else  -- 沒有處理器:預設行為
       error(···)
     end
   end
 end

“sub”: - 操作。 其行為類似於 “add” 操作。
“mul”: * 操作。 其行為類似於 “add” 操作。
“div”: / 操作。 其行為類似於 “add” 操作。
“mod”: % 操作。 其行為類似於 “add” 操作,它的原生操作是這樣的 o1 - floor(o1/o2)*o2
“pow”: ^ (冪)操作。 其行為類似於 “add” 操作,它的原生操作是呼叫 pow 函式(通過 C math 庫)。
“unm”: 一元 - 操作。

 function unm_event (op)
   	local o = tonumber(op)
   if o then  -- 運算元是數字?
     return -o  -- 這裡的 '-' 是一個原生的 'unm'
   else  -- 運算元不是數字。
     -- 嘗試從運算元中得到處理器
     local h = metatable(op).__unm
     if h then
       -- 以運算元為引數呼叫處理器
       return h(op)
     else  -- 沒有處理器:預設行為
       error(···)
     end
   end
 end

“concat”: … (連線)操作,

 function concat_event (op1, op2)
   if (type(op1) == "string" or type(op1) == "number") and
      (type(op2) == "string" or type(op2) == "number") then
     return op1 .. op2  -- 原生字串連線
   else
     local h = getbinhandler(op1, op2, "__concat")
     if h then
       return h(op1, op2)
     else
       error(···)
     end
   end
 end

“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

關於 table 的長度參見 §2.5.5 。

“eq”: == 操作。 函式 getcomphandler 定義了 Lua 怎樣選擇一個處理器來作比較操作。元方法僅僅在參於比較的兩個物件型別相同且有對應操作相同的元方法時才起效。

 function getcomphandler (op1, op2, event)
   if type(op1) ~= type(op2) then return nil end
   local mm1 = metatable(op1)[event]
   local mm2 = metatable(op2)[event]
   if mm1 == mm2 then return mm1 else return nil end
 end

“eq” 事件按如下方式定義:

 function eq_event (op1, op2)
   if type(op1) ~= type(op2) then  -- 不同的型別?
     return false   -- 不同的物件
   end
   if op1 == op2 then   -- 原生的相等比較結果?
     return true   -- 物件相等
   end
   -- 嘗試使用元方法
   local h = getcomphandler(op1, op2, "__eq")
   if h then
     return h(op1, op2)
   else
     return false
   end
 end

a ~= b 等價於 not (a == b) 。

“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

a > b 等價於 b < a.

“le”: <= 操作。

 function le_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, "__le")
     if h then
       return h(op1, op2)
     else
       h = getcomphandler(op1, op2, "__lt")
       if h then
         return not h(op2, op1)
       else
         error(···);
       end
     end
   end
 end

a >= b 等價於 b <= a 。注意,如果元方法 “le” 沒有提供,Lua 就嘗試 “lt” ,它假定 a <= b 等價於 not (b < a) 。

“index”: 取下標操作用於訪問 table[key] 。

 function gettable_event (table, key)
   local h
   if type(table) == "table" then
     local v = rawget(table, key)
     if v ~= nil then return v end
     h = metatable(table).__index
     if h == nil then return nil end
   else
     h = metatable(table).__index
     if h == nil then
       error(···);
     end
   end
   if type(h) == "function" then
     return h(table, key)      -- 呼叫處理器
   else return h[key]          -- 或是重複上述操作
   end
 end

“newindex”: 賦值給指定下標 table[key] = value 。

 function settable_event (table, key, value)
   local h
   if type(table) == "table" then
     local v = rawget(table, key)
     if v ~= nil then rawset(table, key, value); return end
     h = metatable(table).__newindex
     if h == nil then rawset(table, key, value); return end
   else
     h = metatable(table).__newindex
     if h == nil then
       error(···);
     end
   end
   if type(h) == "function" then
     return h(table, key,value)    -- 呼叫處理器
   else h[key] = value             -- 或是重複上述操作
   end
 end

“call”: 當 Lua 呼叫一個值時呼叫。

 function function_event (func, ...)
   if type(func) == "function" then
     return func(...)   -- 原生的呼叫
   else
     local h = metatable(func).__call
     if h then
       return h(func, ...)
     else
       error(···)
     end
   end
 end

相關連結:
lua中文手冊連結速查 - 中國lua開發者 - lua論壇