假设有一个用表来表示集合的模块,该模块还有一些用来计算集合并集和交集等的函数,如下所示:
local Set = {} -- 使用指定的列表创建一个新的集合 function Set.new (l) local set = {} for _, v in ipairs(l) do set[v] = true end return set end function Set.union (a, b) local res = 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(set) local l = {} -- 保存集合中所有元素的列表 for e in pairs(set) do l[#l + 1] = tostring(e) end return "{" .. table.concat(l, ", ") .. "}" end return Set
现在,假设使用加法操作符来计算两个集合的并集,那么可以让所有表示集合的表共享一个元素。这个表中定义了这些表应该如何执行加法操作。首先,我们创建一个普通的表,这个表被用作集合的元表:
local mt = {}
然后,修改用于创建集合的函数 Set.new
。在新版本中只多了一行,即将 mt
设置为函数 Set.new
所创建的表的元表:
function Set.new (l) -- 第二个版本 local set = {} setmetatable(set, mt) for _, v in ipairs(l) do set[v] = true end return set end
在此之后,所有由 Set.new
创建的集合都具有了一个相同的元表:
s1 = Set.new {10,20,30,50} s2 = Set.new{30,1} print(getmetatable(s1)) --> table: 0x00672B60 print(getmetatable(s2)) --> table: 0x00672B60
最后,向元表中加入元方法__add
,也就是用于描述如何完成加法的字段:
mt.__add = Set.union
此后,只要 Lua
语言试图将两个集合相加,他就会调用函数 Set.union
,并将两个操作数作为参数传入。
通过元方法,我们就可以使用加法运算符来计算集合的并集了:
s3 = s1 + s2 print(Set.tostring(s3)) --> {1, 20, 20, 30, 50}
类似的,还可以使用乘法运算符来计算集合的交集:
mt.__mul = Set.intersection print(Set.tostring(s1 + s2) * s1) --> {10, 20, 30, 50}
每种算数运算符和位操作都有一个对应的元方法。如下所示:
算数运算 | 元方法 |
加法 | __add |
乘法 | __mul |
除法 | __div |
减法 | __sub |
floor除法 | __idiv |
负数 | __unm |
取模 | __mod |
幂运算 | __pow |
按位与 | __band |
按位或 | __bor |
按位异或 | __bxor |
按位取反 | __bnot |
向左移位 | __shl |
向右移位 | __shr |
当我们把两个集合相加时,使用那个元表是确定的。然而,当一个表达式中混合了两种具有不同元表的值时,例如:
s = Set.new{1,2,3} s = s + 8
Lua
语言会按照如下步骤来查找元方法:
如果第一个值有元表且元表中存在所需的元方法,那么 Lua
语言就会使用这个元方法,与第二个值无关;如果第二个值有元表且元表中存在所需的元方法, Lua
语言就使用这个元方法;否则, Lua
语言就抛出异常。因此,上例会调用 Set.union
,而表达式 10 + s
和 "hello" + s
同理(由于数值和字符串都没有元方法 __add
)。
Lua
语言不关心这些混合类型,但我们在实现中需要关系混合类型。如果我们执行了 s = s + 8
,那么在 Set.union
内部就会发生错误:
bad argument #1 to 'pairs' (table expected, got number)
如果想要得到更加明确的错误信息,则必须在试图进行操作前显式地检查操作数的类型,例如:
function Set.union(a, b) if getmetatable(a) ~= mt or getmetatable(b) ~= mt then error("attempt to 'add' a set with a non-set value", 2) end -- 同前面
注意
函数 error
的第二个参数说明了出错的原因位于调用该函数的代码中。