咱們可使用操做符對 Lua 的值進行運算,例如對數值類型的值進行加減乘除的運算操做以及對字符串的鏈接、取長操做等(在 Lua 學習筆記(三)—— 表達式 中介紹了許多相似的運算)。元表正是定義這些操做行爲的地方。segmentfault
元表本質上是一個普通 Lua 表。元表中的鍵用來指定操做,稱爲「事件名」;元表中鍵所關聯的值稱爲「元方法」,定義操做的行爲。函數
僅表(table)類型值對應的元表可由用戶自行定義。其餘類型的值所對應的元表僅能經過 Debug 庫進行修改。學習
元表中的事件名均以兩條下劃線 __
做爲前綴,元表支持的事件名有以下幾個:code
__index -- 'table[key]',取下標操做,用於訪問表中的域 __newindex -- 'table[key] = value',賦值操做,增改表中的域 __call -- 'func(args)',函數調用,(參見 《Lua 學習筆記(三)—— 表達式》中的函數部分介紹) -- 數學運算操做符 __add -- '+' __sub -- '-' __mul -- '*' __div -- '/' __mod -- '%' __pow -- '^' __unm -- '-' -- 鏈接操做符 __concat -- '..' -- 取長操做符 __len -- '#' -- 比較操做符 __eq -- '==' __lt -- '<' -- a > b 等價於 b < a __le -- '<=' -- a >= b 等價於 b <= a
還有一些其餘的事件,例如 __tostring
和 __gc
等。對象
下面進行詳細介紹。three
每一個值均可以擁有一個元表。對 userdata 和 table 類型而言,其每一個值均可以擁有獨立的元表,也能夠幾個值共享一個元表。對於其餘類型,一個類型的值共享一個元表。例如全部數值類型的值會共享一個元表。除了字符串類型,其餘類型的值默認是沒有元表的。事件
使用 getmetatable 函數能夠獲取任意值的元表。
使用 setmetatable 函數能夠設置表類型值的元表。(這兩個函數將在[基礎函數庫]部分進行介紹)字符串
只有字符串類型的值默認擁有元表:get
a = "5" b = 5 c = {5} print(getmetatable(a)) --> table: 0x7fe221e06890 print(getmetatable(b)) --> nil print(getmetatable(c)) --> nil
事先提醒 Lua 使用 raw
前綴的函數來操做元方法,避免元方法的循環調用。數學
例如 Lua 獲取對象 obj 中元方法的過程以下:
rawget(getmetatable(obj)or{}, "__"..event_name)
index 是元表中最經常使用的事件,用於值的下標訪問 -- table[key]
。
事件 index 的值能夠是函數也能夠是表。當使用表進行賦值時,元方法可能引起另外一次元方法的調用,具體可見下面僞碼介紹。
當用戶經過鍵值來訪問表時,若是沒有找到鍵對應的值,則會調用對應元表中的此事件。若是 index 使用表進行賦值,則在該表中查找傳入鍵的對應值;若是 index 使用函數進行賦值,則調用該函數,並傳入表和鍵。
Lua 對取下標操做的處理過程用僞碼錶示以下:
function gettable_event (table, key) -- h 表明元表中 index 的值 local h if type(table) == "table" then -- 訪問成功 local v = rawget(table, key) if v ~= nil then return v end -- 訪問不成功則嘗試調用元表的 index h = metatable(table).__index -- 元表不存在返回 nil if h == nil then return nil end else -- 不是對錶進行訪問則直接嘗試元表 h = metatable(table).__index -- 沒法處理致使出錯 if h == nil then error(···); end end -- 根據 index 的值類型處理 if type(h) == "function" then return h(table, key) -- 調用處理器 else return h[key] -- 或是重複上述操做 end end
使用表賦值:
t = {[1] = "cat",[2] = "dog"} print(t[3]) --> nil setmetatable(t, {__index = {[3] = "pig", [4] = "cow", [5] = "duck"}}) print(t[3]) --> pig
使用函數賦值:
t = {[1] = "cat",[2] = "dog"} print(t[3]) --> nil setmetatable(t, {__index = function (table,key) key = key % 2 + 1 return table[key] end}) print(t[3]) --> dog
newindex 用於賦值操做 -- talbe[key] = value
。
事件 newindex 的值能夠是函數也能夠是表。當使用表進行賦值時,元方法可能引起另外一次元方法的調用,具體可見下面僞碼介紹。
當操做類型不是表或者表中尚不存在傳入的鍵時,會調用 newindex 的元方法。若是 newindex 關聯的是一個函數類型之外的值,則再次對該值進行賦值操做。反之,直接調用函數。
~~不是太懂:一旦有了 "newindex" 元方法, Lua 就再也不作最初的賦值操做。 (若是有必要,在元方法內部能夠調用 rawset 來作賦值。)~~
Lua 進行賦值操做時的僞碼以下:
function settable_event (table, key, value) local h if type(table) == "table" then -- 修改表中的 key 對應的 value 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
元方法爲表類型:
t = {} mt = {} setmetatable(t, {__newindex = mt}) t.a = 5 print(t.a) --> nil print(mt.a) --> 5
經過兩次調用 newindex 元方法將新的域添加到了表 mt 。
+++
元方法爲函數:
-- 對不一樣類型的 key 使用不一樣的賦值方式 t = {} setmetatable(t, {__newindex = function (table,key,value) if type(key) == "number" then rawset(table, key, value*value) else rawset(table, key, value) end end}) t.name = "product" t[1] = 5 print(t.name) --> product print(t[1]) --> 25
call 事件用於函數調用 -- function(args)
。
Lua 進行函數調用操做時的僞代碼:
function function_event (func, ...) if type(func) == "function" then return func(...) -- 原生的調用 else -- 若是不是函數類型,則使用 call 元方法進行函數調用 local h = metatable(func).__call if h then return h(func, ...) else error(···) end end end
因爲用戶只能爲表類型的值綁定自定義元表,所以,咱們能夠對錶進行函數調用,而不能把其餘類型的值當函數使用。
-- 把數據記錄到表中,並返回數據處理結果 t = {} setmetatable(t, {__call = function (t,a,b,factor) t.a = 1;t.b = 2;t.factor = factor return (a + b)*factor end}) print(t(1,2,0.1)) --> 0.3 print(t.a) --> 1 print(t.b) --> 2 print(t.factor) --> 0.1
運算操做符相關元方法天然是用來定義運算的。
以 add 爲例,Lua 在實現 add 操做時的僞碼以下:
function add_event (op1, op2) -- 參數可轉化爲數字時,tonumber 返回數字,不然返回 nil 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
代碼中的 getbinhandler 函數定義了 Lua 怎樣選擇一個處理器來做二元操做。 在該函數中,首先,Lua 嘗試第一個操做數。若是這個操做數所屬類型沒有定義這個操做的處理器,而後 Lua 會嘗試第二個操做數。
function getbinhandler (op1, op2, event) return metatable(op1)[event] or metatable(op2)[event] end
+++
對於一元操做符,例如取負,Lua 在實現 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
加法的例子:
t = {} setmetatable(t, {__add = function (a,b) if type(a) == "number" then return b.num + a elseif type(b) == "number" then return a.num + b else return a.num + b.num end end}) t.num = 5 print(t + 3) --> 8
取負的例子:
t = {} setmetatable(t, {__unm = function (a) return -a.num end}) t.num = 5 print(-t) --> -5
對於 tostring 操做,元方法定義了值的字符串表示方式。
例子:
t = {num = "a table"} print(t) --> table: 0x7f8e83c0a820 mt = {__tostring = function(t) return t.num end} setmetatable(t, mt) print(tostring(t)) --> a table print(t) --> a table
對於三種比較類操做,均須要知足兩個操做數爲同類型,且關聯同一個元表時才能使用元方法。
對於 eq (等於)比較操做,若是操做數所屬類型沒有原生的等於比較,則調用元方法。
對於 lt (小於)與 le (小於等於)兩種比較操做,若是兩個操做數同爲數值或者同爲字符串,則直接進行比較,不然使用元方法。
對於 le 操做,若是元方法 "le" 沒有提供,Lua 就嘗試 "lt",它假定 a <= b 等價於 not (b < a) 。
等於比較操做:
t = {name="number",1,2,3} t2 = {name = "number",4,5,6} mt = {__eq = function (a,b) return a.name == b.name end} setmetatable(t,mt) -- 必需要關聯同一個元表才能比較 setmetatable(t2,mt) print(t==t2) --> true
對於鏈接操做,當操做數中存在數值或字符串之外的類型時調用該元方法。
對於取長操做,若是操做數不是字符串類型,也不是表類型,則嘗試使用元方法(這致使自定義的取長基本沒有,在以後的版本中彷佛作了改進)。
取長操做:
t = {1,2,3,"one","two","three"} setmetatable(t, {__len = function (t) local cnt = 0 for k,v in pairs(t) do if type(v) == "number" then cnt = cnt + 1 print(k,v) end end return cnt end}) -- 結果是 6 而不是預期中的 3 print(#t) --> 6