现在的位置: 首页 > 综合 > 正文

Lua元表与元方法

2019年01月09日 ⁄ 综合 ⁄ 共 3428字 ⁄ 字号 评论关闭

前言

元表对应的英文是metatable,元方法是metamethod。我们都知道,在C++中,两个类是无法直接相加的,但是,如果你重载了“+”符号,就可以进行类的加法运算。在Lua中也有这个道理,两个table类型的变量,你是无法直接进行“+”操作的,如果你定义了一个指定的函数,就可以进行了。

Lua是怎么做的?

通常,Lua中的每个类型的值都有一套预定义的操作集合,比如数字是可以相加的,字符串是可以连接的,但是对于两个table类型,则不能直接进行“+”操作。这需要我们进行一些操作。在Lua中有一个元表(metastable),我们可以通过元表来修改一个值得行为,使其在面对一个非预定义的操作时执行一个指定的操作。比如,现在有两个table类型的变量a和b,我们可以通过metatable定义如何计算表达式a+b,具体的在Lua中是按照以下步骤进行的:

  1. 先判断a和b两者之一是否有元表;
  2. 检查该元表中是否有一个叫__add的字段;
  3. 如果找到了该字段,就调用该字段对应的值,这个值对应的是一个metamethod;
  4. 调用__add对应的metamethod计算a和b的值。

上述四个步骤就是计算table类型变量a+b的过程。在Lua中,每个值都有一个元表,table和userdata类型的每个变量都可以有各自独立的元表,而其他类型的值则共享其类型所属的单一元表任何一个表都可以是其他一个表的metatable,一组相关的表可以共享一个metatable(描述他们共同的行为)。一个表也可以是自身的metatable(描述其私有行为)。

算术运算的元方法

Set = {} --对于集合的操作有,取并集和取交集; 对于两个集合的关系有,是否包含,是否相等
Set.mt = {} --集合的元表

--根据参数列表中的值创建一个新的集合
function Set.new(t)
    local set = {}
    setmetatable(set,Set.mt) --Set.new创建的所有集合都有相同的metatable
    for _,v in pairs(t) do set[v] = true end
    return set
end

--并集操作
function Set.union(a, b)     
    local res = Set.new{} --相当于Set.new({})
    for k in pairs(a) do res[k] = true end
    for k in pairs(b) do res[k] = true end
    return res
end

--交集操作
function Set.intersection(a, b)
    local res = Set.new({})
    for k in pairs(a) do res[k] = b[k] end
    return res
end

--打印集合的操作
function Set.tostring(t)
    local tb = {}
    for k in pairs(t) do
        tb[#tb + 1] = k
    end
    return "{" .. table.concat(tb,", ") .. "}"
end

function Set.print(t)
    print(Set.tostring(t))
end

local s1 = Set.new({10, 20, 30, 50})
local s2 = Set.new({30, 1})

print(getmetatable(s1)) -->table: 03B90CD0
print(getmetatable(s2)) -->table: 03B90CD0,说明s1与s2有相同的元表

Set.mt.__add = Set.union --给metatable增加__add函数,来去两个集合的全集
Set.print(s1 + s2) -->{1, 30, 10, 50, 20}

Set.mt.__mul = Set.intersection --给metatable增加__mul函数,来去两个集合的交集
Set.print((s1 + s2)*s1) -->{30, 10, 50, 20}

对于每一个算术运算符,metatable都有对应的域名与其对应,除了__add(加)、__mul(乘)外,还有__sub(减)、__div(除)、__unm(负)、__pow(幂),我们也可以定义__concat定义连接行为。
当我们对两个表进行加没有问题,但如果两个操作数有不同的metatable例如:local s3 = s1 + 8 
Lua选择metamethod的原则:如果第一个参数存在带有__add域的metatable,Lua使用它作为metamethod,和第二个参数无关;
否则第二个参数存在带有__add域的metatable,Lua使用它作为metamethod 否则报错。
Lua不关心这种混合类型的,
如果我们运行上面的s3=s1+8的例子在Set.union发生错误:
bad argument #1 to `pairs' (table expected, got number)

function Set.union(a, b) 
    if getmetatable(a) ~= Set.mt or getmetatable(b) ~= Set.mt then
        error("attempt to 'add' a set with a non-set value",2)
    end
    local res = Set.new{} --相当于Set.new({})
    for k in pairs(a) do res[k] = true end
    for k in pairs(b) do res[k] = true end
    return res
end 

关系运算的元方法

Metatables也允许我们使用metamethods:__eq(等于),__lt(小于),和__le(小于等于)给关系运算符赋予特殊的含义。对剩下的三个关系运算符没有专门的metamethod,因为Lua将a~= b转换为not (a == b);a > b转换为b < a;a >= b转换为 b <= a。

(直到Lua 4.0为止,所有的比较运算符被转换成一个,a <= b转为not (b < a)。然而这种转换并不一致正确。当我们遇到偏序(partialorder)情况,也就是说,并不是所有的元素都可以正确的被排序情况。例如,在大多数机器上浮点数不能被排序,因为他的值不是一个数字(Not a Number即NaN)。根据IEEE754的标准,NaN表示一个未定义的值,比如0/0的结果。该标准指出任何涉及到NaN比较的结果都应为false。也就是说,NaN
<= x总是false,x< NaN也总是false。这样一来,在这种情况下a<= b 转换为 not (b < a)就不再正确了。)

在我们关于集合操作的例子中,有类似的问题存在。<=代表集合的包含:a <= b表示集合a是集合b的子集。这种意义下,可能a<= b和b < a都是false;因此,我们需要将__le和__lt的实现分开: 

--判断两个集合的包含关系(小于等于)--子集关系
Set.mt.__le = function (a,b)
    for k in pairs(a) do
        if not b[k] then
            return false
        end
    end
    return true
end

--判断两个集合是否为真子集关系
Set.mt.__lt = function(a,b)
    return a <= b and not (b <= a)
end

--判断两个集合是否相等
Set.mt.__eq = function(a,b)
    return a <= b and b <= a
end

local s4 = Set.new({2,4})
local s5 = Set.new({4,10,2})

print(s4 <= s5)      --true
print(s4 < s5)       --true
print(s4 >= s4)      --true
print(s4 > s4)       --false 
print(s4 == s4 * s5) --true

与算术运算的metamethods不同,关系元算的metamethods不支持混合类型运算。对于混合类型比较运算的处理方法和Lua的公共行为类似。如果你试图比较一个字符串和一个数字,Lua将抛出错误。相似的,如果你试图比较两个带有不同metamethods的对象,Lua也将抛出错误。

但相等比较从来不会抛出错误,如果两个对象有不同的metamethod,比较的结果为false,甚至可能不会调用metamethod。这也是模仿了Lua的公共的行为,因为Lua总是认为字符串和数字是不等的,而不去判断它们的值。仅当两个有共同的metamethod的对象进行相等比较的时候,Lua才会调用对应的metamethod。

抱歉!评论已关闭.