元表与元方法
通常,Lua 中的每个值都有一套预定义的操作集合。例如,可以将数字相加,可以连接字符串,还可以在 table 中插入一对 key-value 等。但是我们无法将两个 table 相加,无法对函数做比较,也无法调用一个字符串。
但是我们可以通过元表来修改一个值的行为,使其执行一个非预定义的操作时能执行一个指定的操作。比如,如果 a 和 b 都是 table ,我们调用 setmetatable
函数将 a 的元表设置为 table t,其中 t 的 __add
字段 (也称key为"__add",或者 index 为“__add”) 的值是一个函数,那么 Lua 在执行 a + b 表达式时就会调用这个函数执行相加的操作。
任何 table 都可以作为任何值的元表。在 Lua 代码中,只能设置 table 的元表,若要设置其他类型的值的元表,则必须通过 C 代码来完成。标准字符串库为所有的字符串都设置了一个元表,而其他类型的值默认都没有元表。
算术类和关系类的元方法
下面是一个用 table 表示集合的示例,说明如何使用元表。
Set = {}
-- 集合的元表
local mt = {}
-- 根据参数列表中的值创建一个集合
function Set.new(l)
local set = {}
-- 设置元表为mt
setmetatable(set, mt)
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
-- 集合b是否包含集合a
function Set.contain(a,b)
for k in pairs(a) do
if not b[k] then return false end
end
return true
end
-- 打印集合
function Set.tostring(set)
local l = {}
for e in pairs(set) do l[#l + 1] = e end
return "{" .. table.concat(l, ",") .. "}"
end
-- 为元表mt设置元方法
-- a + b,集合并集
mt.__add = Set.union
-- a * b,集合交集
mt.__mul = Set.intersection
-- a <= b,集合b是否包含集合a
mt.__le = Set.contain
-- a < b,集合b是否包含集合b(a,b集合不相同)
mt.__lt = function (a,b)
return a <= b and not (b <= a)
end
-- a == b,集合是否相等
mt.__eq = function (a,b)
return a <= b and b <= a
end
-- print打印集合
mt.__tostring = Set.tostring
s1 = Set.new{1,2,3}
s2 = Set.new{3}
-- 输出并集
print(s1 + s2)
-- 输出交集
print(s1 * s2)
print(s2 < s1)
print(s1 == s2)
当两个集合相加时,可以使用任意一个集合的元表。然而,如果一个表达式中混合了具有不同元表的值时,例如:
s = Set.new{1,2,3}
s = s + 8
Lua 会按照如下步骤来查找元表:如果第一个值有元表,并且元表中有 __add 元方法,那么就使用这个元方法。如果第一个值的元表中没有 __add 元方法,则使用第二个值的 __add 元方法。如果元表中都没有 __add 元方法,则报错。
函数 setmetatable
和 getmetatable
也会用到元表中的一个字段,用于保护元表。假设想要保护集合的元表,使用户既不能看到也不能修改集合元表,那么就需要为元表设置 __metatable
字段,当设置了该字段时,getmetatable
会返回该字段的值,而 setmetatable
则会引发一个错误。
mt.__metatable = "not your business"
s1 = Set.new{}
print(getmetatable(s1))
setmetatable(s1, {})
上述代码会有如下输出:
not your business
lua: set.lua:80: cannot change a protected metatable
stack traceback:
[C]: in function 'setmetatable'
set.lua:80: in main chunk
[C]: ?
table 访问的元方法
__index 元方法
当访问一个 table 中不存在的字段时,得到的结果为 nil
。这并非完全正确。实际上,Lua 在找不到一个字段时,会去查找 __index
元方法,如果没有这个元方法,则返回 nil
。否则,就由这个元方法提供返回结果。
在 Lua 中,将 __index
元方法用于继承是很普遍的做法,因此 Lua 提供了一种更便捷的方法来实现此功能。那就是 __index
元方法不一定必须是一个函数,它还可以是一个 table 。当它是一个函数时,Lua 以 table 和不存在的 key 作为参数来调用该函数。而当它是一个 table 时,Lua 就以相同的方式来访问这个 table 。
如果不想在访问一个 table 时触发它的 __index
元方法,可以使用 rawget
函数。
__newindex 元方法
__index
元方法用于 table 的查找,而 __newindex
元方法用于 table 的更新。当对一个 table 中不存在的字段赋值时,Lua 就会查找 __newindex
元方法,并调用它,而不是执行赋值。如果这个元方法是一个 table ,Lua 就在这个 table 中执行赋值操作。使用 rawset
函数可以绕过 __newindex
元方法,直接设置 key-value。
具有默认值的 table
常规 table 中的任何字段的默认值都是 nil
。通过元表可以很容易地修改这个默认值:
function setDefault(t,d)
local mt = {__index = function () return d end}
setmetatable(t, mt)
end
t = {x=10,y=20}
print(t.x, t.z) ---> 10 nil
setDefault(t, 0)
print(t.x, t.z) ---> 10 0
若要让具有不同默认值的 table 都使用一个元表,那么就需要将每个 table 的默认值放到 table 中,这个默认值的字段必须是唯一的。为了保持默认值字段的唯一性,我们可以创建一个新的 table ,并使用它作为 key 。
local key = {}
local mt = {__index = function (t) return t[key] end}
function setDefault(t,d)
t[key] = d
setmetatable(t, mt)
end
跟踪 table 的访问
__index
和 __newindex
元方法都是在 table 中没有所需访问的字段时才发挥作用。为了监视一个 table 中的所有访问,我们可以为真正的 table 创建一个代理,这个代理是一个空 table,其中 __index
和 __newindex
元方法可用于跟踪所有的访问,并将访问重定向到真正的 table 上。假设我们要跟踪 table t 的所有访问,我们可以这么做:
-- t 是在某个地方创建的 table
t = {}
-- 保持对原有 table 的一个私有访问
local _t = t
-- 创建代理
t = {}
-- 创建元表
local mt = {
__index = function (t, k)
print("access element " .. tostring(k))
-- 返回真正的 table 中的值
return _t[k]
end,
__newindex = function (t, k, v)
print("update element " .. tostring(k) .. " to " .. tostring(v))
-- 更新真正的 table 中的值
_t[k] = v
end
}
-- 设置元表
setmetatable(t, mt)
这个方法存在一个问题就是无法遍历原来的 table 。
如果想要同时监视几个 table ,只需要以某种形式将每个代理与原 table 关联起来,并且所有的代理都共享一个元表。我们仍可以使用在具有默认值的 table 一节中的方法,将原来的 table 保存在代理 table 中的一个唯一字段中。代码如下:
-- 创建唯一的 index
local index = {}
-- 创建元表
local mt = {
__index = function (t, k)
print("access element " .. tostring(k))
-- 返回真正的 table 中的值
return t[index][k]
end,
__newindex = function (t, k, v)
print("update element " .. tostring(k) .. " to " .. tostring(v))
-- 更新真正的 table 中的值
t[index][k] = v
end
}
function track(t)
local proxy = {}
proxy[index] = t
setmetatable(proxy, mt)
return proxy
end
-- 原来的 table
t = {}
t = track(t)
只读的 table
要实现只读的 table 很容易,只需要创建一个代理,跟踪所有对 table 的更新操作,并引发一个错误就可以了。由于无需跟踪字段的访问,我们可以直接将 __index
元方法设置称原来的 table 。不过这种方法要求为每个代理创建一个新的元表。代码如下:
function readOnly(t)
local proxy = {}
local mt = {
__index = t,
__newindex = function (t, k , v)
error("attempt to update a read-only table", 2)
end
}
setmetatable(proxy, mt)
return proxy
end
days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
print(days[1])
days[2] = "Noday"