Chapter 13: Metatables and Metamethods
我们可以加两个数,连接字符串,往表中插入key-value 对,但我们不能对表执行加法,不能比较两个函数,不能call 字符串。
元表(Metatables) 充许我们改变值的行为,当通到未定义操作。例如,使用元表我们能定义Lua 如何计算“表a + 表b”。lua 会检查它们是否有metatable,这个metatable 是抵抗有一个__add 域。如果找到这个域,就调用相对应的值,既元方法metamethod。由元方法完成加法计算。
Lua 中所有值都可以有元表。Table 和userdata 拥有独立的元表。其它类型的值共享单个元表。
新创建的表不带元表:
t = {}
print(getmetatable(t)) --> nil
为表设置元表:
t = {}
print(getmetatable(t)) --> nil
t1 = {}
setmetatable(t, t1)
assert(getmetatable(t) == t1)
print(getmetatable(t)) --> table: 003CACD0
一组相关表可以共享同一个元表,它描述了这些表的共同行为。表可以作为自已的元表。
Lua 中只能用table 作为元表,如果使用C 代码则可突破此限制。第20 章我们会看到,string 库为string 设置了元表,所有其它类型默认没有元表。
13.1 Arithmetic Metamethods
这一节,我们引入一个简单的例子解释如何使用元表。假设我们使用表来表示集合,并用函数去计算两集合的并集,交集,等。为保持命名空间的清洁,我们存储这些函数在表的内部:
Set = {}
-- create a new set with the values of the given list
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 = {} -- list to put all elements from the set
for e in pairs(set) do
l[#l + 1] = e
end
return "{" .. table.concat(l, ", ") .. "}"
end
function Set.print (s)
print(Set.tostring(s))
end
现在,我们想用操作符“+” 计算两个集合的并。我们要让所有支持这种操作的表共享同一个元表。第一步,创建一个普通的表作为所有这些表的元表。
local mt = {} -- metatable for sets
第二步,修改Set.new,让它创建表后设置其元表为mt。
function Set.new (l) -- 2nd version
local set = {}
setmetatable(set, mt)
for _, v in ipairs(l) do set[v] = true end
return set
end
最后,为元表添加元方法,__add 域描述了如何执行加法操作:
mt.__add = Set.union
s3 = s1 + s2
Set.print(s3) --> {1, 10, 20, 30, 50}
类似的,可以添加乘操作:
mt.__mul = Set.intersection
Set.print((s1 + s2)*s1) --> {10, 20, 30, 50}
基于元表的“运算符重载”
Set = {}
local mt = {} -- metatable for sets
-- create a new set with the values of the given list
function Set.new (l) -- 2nd version
local set = {}
setmetatable(set, mt)
for _, v in ipairs(l) do set[v] = true end
return set
end
function Set.union (a, b)
if getmetatable(a) ~= mt or getmetatable(b) ~= mt then -- 检查a,b 是否具有相同的元表
error("attempt to 'add' a set with a non-set value", 2)
end
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 = {} -- list to put all elements from the set
for e in pairs(set) do
l[#l + 1] = e
end
return "{" .. table.concat(l, ", ") .. "}"
end
function Set.print (s)
print(Set.tostring(s))
end
s1 = Set.new{10, 20, 30, 50}
s2 = Set.new{30, 1}
print(getmetatable(s1)) --> table: 00672B60
print(getmetatable(s2)) --> table: 00672B60
mt.__add = Set.union
s3 = s1 + s2
Set.print(s3) --> {1, 10, 20, 30, 50}
mt.__mul = Set.intersection
Set.print((s1 + s2)*s1) --> {10, 20, 30, 50}
13.2 Relational Metamethods
Set = {}
local mt = {} -- metatable for sets
-- create a new set with the values of the given list
function Set.new (l) -- 2nd version
local set = {}
setmetatable(set, mt)
for _, v in ipairs(l) do set[v] = true end
return set
end
function Set.union (a, b)
if getmetatable(a) ~= mt or getmetatable(b) ~= mt then -- 检查a,b 是否具有相同的元表
error("attempt to 'add' a set with a non-set value", 2)
end
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 = {} -- list to put all elements from the set
for e in pairs(set) do
l[#l + 1] = e
end
return "{" .. table.concat(l, ", ") .. "}"
end
function Set.print (s)
print(Set.tostring(s))
end
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
mt.__lt = function (a, b)
return a <= b and not (b <= a)
end
mt.__eq = function (a, b)
return a <= b and b <= a
end
mt.__add = Set.union
mt.__mul = Set.intersection
s1 = Set.new{2, 4}
s2 = Set.new{4, 10, 2}
print(s1 <= s2) --> true -- s1 是s2 的子集
print(s1 < s2) --> true
print(s1 >= s1) --> true
print(s1 > s1) --> false
print(s1 == s2 * s1) --> true
13.3 Library-Defined Metamethods
printf 首先检查传给它的值是否存在元方法__tostring。
print({}) --> table: 0x8062ac0
mt.__tostring = Set.tostring
s1 = Set.new{10, 4, 5}
print(s1) --> {4, 5, 10}
保护元表
mt.__metatable = "not your business"
s1 = Set.new{}
print(getmetatable(s1)) --> not your business
setmetatable(s1, {})
stdin:1: cannot change protected metatable
13.4 Table-Access Metamethods
The __index metamethod
通常访问表中不存在的域会得到nil 值。而实际上,在内部会查找__index 元方法,如果确实不存在__index 方法就返回nil,否则返回值就由这个元方法给出。
Window = {} -- create a namespace
-- create the prototype with default values
Window.prototype = {x=0, y=0, width=100, height=100}
Window.mt = {} -- create a metatable
-- declare the constructor function
function Window.new (o)
setmetatable(o, Window.mt)
return o
end
Window.mt.__index = function (table, key)
return Window.prototype[key]
end
w = Window.new{x=10, y=20}
print(w.width) --> 100
__index 元方法既可以是一个函数也可以是一个表
Window = {} -- create a namespace
-- create the prototype with default values
Window.prototype = {x=0, y=0, width=100, height=100}
Window.mt = {} -- create a metatable
-- declare the constructor function
function Window.new (o)
setmetatable(o, Window.mt)
return o
end
Window.mt.__index = Window.prototype
w = Window.new{x=10, y=20}
print(w.width) --> 100 -- w.width 相当于访问 Window.prototype["width"]
使用表作为 __index metamethod 提供了一种快速简单的方法实现单继承。使用函数虽然开销大些,也更灵活:可以实现多继承,caching,和其它变化形式。第16 章会讨论这些变化形式。
rawget 函数实现非__index 访问
rawget(t,i) , raw access 不会提高速度,但有时你会需要它,以后会看到。
The __newindex metamethod
t[“key”] = value 当”key” 这个索引不存在时,内部会查看是否存在__newindex 元方法,如果有则调用这个元方法来代替赋值操作。__newindex 也可以是表,如果是这样这个赋值操作就是作用于这个表的,而不是原来那个。同样,rawset(t,k,v) 是不考虑元方法的赋值操作。
组合使用__index 和__newindex 元方法可以实现只读表,带默认值的表,面向对象的继承等。面象对象编程有专门的章节讨论。
具有默认值的表
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
同一个元表不同默认值
可以让各个表自已存自已的默认值。如果不担心命名冲突可以给每个表额外加一个___域:
local mt = {__index = function (t) return t.___ end}
function setDefault (t, d)
t.___ = d
setmetatable(t, mt)
end
不会产生命名冲突的解决方案要等到第17 章,因为需要weak tables 或是memoize metatables。
local key = {} -- unique key
local mt = {__index = function (t) return t[key] end}
function setDefault (t, d)
t[key] = d
setmetatable(t, mt)
end
Tracking table accesses
追踪对表的访问需要一个“代理表”来代理对真实表的所有访问。代理表是一个空表,有适当的__index 和__newindex 元方法,它跟踪所有访问并将它们重定向到原始表。假设
t 是我们想要追踪的原始表:
t = {} -- original table (created somewhere)
-- keep a private access to the original table
local _t = t
-- create proxy
t = {}
-- create metatable
local mt = {
__index = function (t, k)
print("*access to element " .. tostring(k))
return _t[k] -- access the original table
end,
__newindex = function (t, k, v)
print("*update of element " .. tostring(k) ..
" to " .. tostring(v))
_t[k] = v -- update original table
end
}
setmetatable(t, mt)
t[2] = "hello"
print(t[2])
(注意到,很不幸的,这种机制使得我们无法遍历表,pairs 函数将作用于代理表,而不是原始表)
如果我们想要监视多个表,我们不需要给每个表都设置不同的元表。将原始表存在代理表里面:
监视多个表
t = {}
t2 ={}
local index = {} -- create private index
local mt = { -- create metatable
__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=track(t)
t2=track(t2)
print(t[1])
print(t2[2])
Read-only tables
只读表
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
days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
print(days[1]) --> Sunday
days[2] = "Noday" -- stdin:1: attempt to update a read-only table