Lua 学习笔记(四)—— 元表与元方法

我们可以使用操作符对 Lua 的值进行运算,例如对数值类型的值进行加减乘除的运算操作以及对字符串的连接、取长操作等(在 Lua 学习笔记(三)—— 表达式 中介绍了许多类似的运算)。元表正是定义这些操作行为的地方。

元表本质上是一个普通 Lua 表。元表中的键用来指定操作,称为“事件名”;元表中键所关联的值称为“元方法”,定义操作的行为。

1 事件名与元方法

仅表(table)类型值对应的元表可由用户自行定义。其他类型的值所对应的元表仅能通过 Debug 库进行修改。

元表中的事件名均以两条下划线 __ 作为前缀,元表支持的事件名有如下几个:

__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 等。

下面进行详细介绍。

2 元表与值

每个值都可以拥有一个元表。对 userdata 和 table 类型而言,其每个值都可以拥有独立的元表,也可以几个值共享一个元表。对于其他类型,一个类型的值共享一个元表。例如所有数值类型的值会共享一个元表。除了字符串类型,其他类型的值默认是没有元表的。

使用 getmetatable 函数可以获取任意值的元表。
使用 setmetatable 函数可以设置表类型值的元表。(这两个函数将在[基础函数库]部分进行介绍)

2.1 例子

只有字符串类型的值默认拥有元表:

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 
    原文作者:tangyikejun
    原文地址: https://segmentfault.com/a/1190000002723537
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞