1.元表、元操作
1.1算术元操作
Metatables允许我们改变table的行为,例如,使用Metatables我们可以定义Lua如何计算两个table的相加操作a+b。当Lua试图对两个表进行相加时,他会检查两个表是否有一个表有Metatable,并且检查Metatable是否有__add域。如果找到则调用这个__add函数(所谓的Metamethod)去计算结果。
Lua中的每一个表都有其Metatable。(后面我们将看到userdata也有Metatable),Lua默认创建一个不带metatable的新表
t = {}
print(getmetatable(t)) --> nil
Set.mt = {} -- metatable for sets
function Set.new (t) -- 2nd version --创建
local set = {}
setmetatable(set, Set.mt)
for _, l in ipairs(t) do set[l] = 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 s = "{"
local sep = ""
for e in pairs(set) do
s = s .. sep .. e
sep = ", "
end
return s .. "}"
end
function Set.print (s) --打印
print(Set.tostring(s))
end
第一步,我们定义一个普通的表,用来作为metatable。为避免污染命名空间,我们将其放在set内部。
Set.mt = {} -- metatable for sets
第二步,给metatable增加__add函数。
Set.mt.__add = Set.union
当Lua试图对两个集合相加时,将调用这个函数,以两个相加的表作为参数。
通过metamethod,我们可以对两个集合进行相加:
s3 = s1 + s2
Set.print(s3) --> {1, 10, 20, 30, 50}
同样的我们可以使用相乘运算符来定义集合的交集操作
Set.mt.__mul = Set.intersection
Set.print((s1 + s2)*s1) --> {10, 20, 30, 50}
Lua选择metamethod的原则:如果第一个参数存在带有__add域的metatable,Lua使用它作为metamethod,和第二个参数无关;
否则第二个参数存在带有__add域的metatable,Lua使用它作为metamethod 否则报错。
1.2关系元操作
Set.mt.__le = function (a,b) -- set containment -- <
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
与算术运算的 metamethods 不同,关系元算的 metamethods 不支持混合类型运算。对于混合类型比较运算的处理方法和 Lua 的公共行为类似。1.3库定义元操作
setmetatable/getmetatable函数也会使用metafield,在这种情况下,可以保护metatables。假定你想保护你的集合使其使用者既看不到也不能修改metatables。如果你对metatable设置了__metatable的值,getmetatable将返回这个域的值,而调用setmetatable 将会出错:
Set.mt.__metatable = "not your business"
s1 = Set.new{}
print(getmetatable(s1)) --> not your business
setmetatable(s1, {})
stdin:1: cannot change protected metatable
1.4有默认的表值元操作
在一个普通的表中任何域的默认值都是nil。很容易通过metatables来改变默认值:
function setDefault (t, d)
local mt = {__index = function () return d end}
setmetatable(t, mt)
end
tab = {x=10, y=20}
print(tab.x, tab.z) --> 10 nil
setDefault(tab, 0)
print(tab.x, tab.z) --> 10 0
1.5监控表
我们只要将每一个proxy和他原始的表关联,所有的proxy共享一个公用的metatable即可。将表和对应的proxy关联的一个简单的方法是将原始的表作为proxy的域,只要我们保证这个域不用作其他用途。一个简单的保证它不被作他用的方法是创建一个私有的没有他人可以访问的key。将上面的思想汇总,最终的结果如下:
-- create private index
local index = {}
-- create metatable
local mt = {
__index = function (t,k)
print("*access to element " .. tostring(k))
return t[index][k] -- access the original table
end
__newindex = function (t,k,v)
print("*update of element " .. tostring(k) .. " to "
.. tostring(v))
t[index][k] = v -- update original table
end
}
function track (t)
local proxy = {}
proxy[index] = t
setmetatable(proxy, mt)
return proxy
end
现在,不管什么时候我们想监控表t,我们要做得只是t=track(t)。
1.6只读表 |
采用代理的思想很容易实现一个只读表。我们需要做得只是当我们监控到企图修改表时候抛出错误。通过__index metamethod,我们可以不使用函数而是用原始表本身来使用表,因为我们不需要监控查寻。这是比较简单并且高效的重定向所有查询到原始表的方法。但是,这种用法要求每一个只读代理有一个单独的新的metatable,使用__index指向原始表:
function readOnly (t)
local proxy = {}
local mt = { -- create metatable
__index = t,
__newindex = function (t,k,v)
error("attempt to update a read-only table", 2)
end
}
setmetatable(proxy, mt)
return proxy
end