Lua 學習筆記(四)—— 元表與元方法

咱們可使用操做符對 Lua 的值進行運算,例如對數值類型的值進行加減乘除的運算操做以及對字符串的鏈接、取長操做等(在 Lua 學習筆記(三)—— 表達式 中介紹了許多相似的運算)。元表正是定義這些操做行爲的地方。segmentfault

元表本質上是一個普通 Lua 表。元表中的鍵用來指定操做,稱爲「事件名」;元表中鍵所關聯的值稱爲「元方法」,定義操做的行爲。函數

1 事件名與元方法

僅表(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

2 元表與值

每一個值均可以擁有一個元表。對 userdata 和 table 類型而言,其每一個值均可以擁有獨立的元表,也能夠幾個值共享一個元表。對於其餘類型,一個類型的值共享一個元表。例如全部數值類型的值會共享一個元表。除了字符串類型,其餘類型的值默認是沒有元表的。事件

使用 getmetatable 函數能夠獲取任意值的元表。
使用 setmetatable 函數能夠設置表類型值的元表。(這兩個函數將在[基礎函數庫]部分進行介紹)字符串

2.1 例子

只有字符串類型的值默認擁有元表:get

a = "5"
b = 5
c = {5}
print(getmetatable(a))      --> table: 0x7fe221e06890
print(getmetatable(b))      --> nil
print(getmetatable(c))      --> nil

3 事件的具體介紹

事先提醒 Lua 使用 raw 前綴的函數來操做元方法,避免元方法的循環調用。數學

例如 Lua 獲取對象 obj 中元方法的過程以下:

rawget(getmetatable(obj)or{}, "__"..event_name)

3.1 元方法 index

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

3.1.1 例子

使用表賦值:

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

3.2 元方法 newindex

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

3.2.1 例子

元方法爲表類型:

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

3.3 元方法 call

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

3.3.1 例子

因爲用戶只能爲表類型的值綁定自定義元表,所以,咱們能夠對錶進行函數調用,而不能把其餘類型的值當函數使用。

-- 把數據記錄到表中,並返回數據處理結果
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

3.4 運算操做符相關元方法

運算操做符相關元方法天然是用來定義運算的。

以 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

3.4.1 例子

加法的例子:

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

3.5 元方法 tostring

對於 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

3.6 比較類元方法

對於三種比較類操做,均須要知足兩個操做數爲同類型,且關聯同一個元表時才能使用元方法。

對於 eq (等於)比較操做,若是操做數所屬類型沒有原生的等於比較,則調用元方法。

對於 lt (小於)與 le (小於等於)兩種比較操做,若是兩個操做數同爲數值或者同爲字符串,則直接進行比較,不然使用元方法。

對於 le 操做,若是元方法 "le" 沒有提供,Lua 就嘗試 "lt",它假定 a <= b 等價於 not (b < a) 。

3.6.1 例子

等於比較操做:

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

3.7 其餘事件的元方法

對於鏈接操做,當操做數中存在數值或字符串之外的類型時調用該元方法。

對於取長操做,若是操做數不是字符串類型,也不是表類型,則嘗試使用元方法(這致使自定義的取長基本沒有,在以後的版本中彷佛作了改進)。

3.7.1 例子

取長操做:

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
相關文章
相關標籤/搜索