Q:什么是”metatable”?
A:”metatable”是一张定义了一些特殊功能的”table”,当他被分配给某个”table”(另一个”table”,与”metatable”不是同一个”table”)时,这个”table”就具有了这些特殊功能。
当在Lua中创建一个”table”时,默认不会创建它的”metatable”。使用setmetatable()
为一个”table”分配”metatable”;使用getmetatable()
获取分配给”table”的”metatable”。
t = {}
print(getmetatable(t)) --> nil
mt = {}
setmetatable(t, mt)
print(getmetatable(t) == mt) --> true
Q:什么是”metamethod”?
A:”metatable”所具有的特殊功能就是”metamethod”,比如”metatable”的”__add”,”__sub”,”__eq”等等。
举个例子,通常在Lua中对”table”的操作是:插入一个”key-value”对,通过”key”查找”value”,遍历所有的”key-value”对。但是通常不能将两个”table”相加,比较两个”table”的大小等等,而”metamethod”就能够完成这些操作。
mt.__add = function () ... end -- 定义两个"table"相加的逻辑。
mt.__sub = function () ... end -- 定义两个"table"相减的逻辑。
mt.__eq = function () ... end -- 定义判断两个"table"是否相等的逻辑。
...
Q:如何定义算数运算的”metamethods”?
A:以计算两个集合之间的算数运算为例,
Set = {} -- 存储对于集合的所有操作函数。
Set.mt = {} -- "metatable"。
-- 创建一个新的集合。
function Set.new (t)
local set = {} -- 新的集合。
setmetatable(set, Set.mt) -- 所有的集合共享同一个"metatable"。
-- "key-value",集合中的元素值-"true"。
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] -- 很聪明的方法,如果集合"b"中没有元素"k"的话就会返回"nil"。
end
return res
end
-- 我们规定"s1 - s2"相当于求"(s1 * s2)"在"s1"中的补集。
function Set.sub(a, b)
local res = Set.new{}
for k in pairs(a) do
if not b[k] then
res[k] = true
end
end
return res
end
-- 我们规定"s1 / s2",仅是打印一句话,以证明调用了正确的"metamethod"。
function Set.div(a, b)
local res = Set.new{"__div"}
return res
end
-- 我们规定"-s"仅是打印一句话,以证明调用了正确的"metamethod"。
function Set.unm(a)
local res = Set.new{"__unm"}
return res
end
-- 我们规定"s1 ^ s2"仅是打印一句话,以证明调用了正确的"metamethod"。
function Set.pow(a, b)
local res = Set.new{"__pow"}
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
Set.mt.__add = Set.union -- 定义两个"table"相加的逻辑,并集。
Set.mt.__mul = Set.intersection -- 定义两个"table"相乘的逻辑,交集。
Set.mt.__sub = Set.sub -- 定义两个"table"相减的逻辑。
Set.mt.__div = Set.div -- 定义两个"table"相除的逻辑。
Set.mt.__unm = Set.unm -- 定义一个"table"取反的逻辑。
Set.mt.__pow = Set.pow -- 定义两个"table"相幂的逻辑。
s1 = Set.new{10, 20, 30, 50}
s2 = Set.new{30, 1}
-- 两个集合共享同一个"metatable"。
print(getmetatable(s1) == getmetatable(s2)) --> true
Set.print(s1 + s2) --> {1, 10, 20, 30, 50}
Set.print(s1 * s2) --> {30}
Set.print(s1 - s2) --> {20, 50, 10}
Set.print(s1 / s2) --> {__div}
Set.print(-s2) --> {__unm}
Set.print(s1 ^ s2) --> {__pow}
Q:如何定义关系运算的”metamethods”?
A:继续借用上面”Q & A”中的例子,
-- 定义两个"table"比较大小的逻辑。"a"是否是"b"的子集。
Set.mt.__le = function (a,b)
for k in pairs(a) do
if not b[k] then return false end
end
return true
end
-- 定义两个"table"比较大小的逻辑。"a"是否是"b"的真子集。
Set.mt.__lt = function (a,b)
return a <= b and not (b <= a)
end
-- 定义两个"table"比较大小的逻辑。两个集合是否相等。
Set.mt.__eq = function (a,b)
return a <= b and b <= a
end
s1 = Set.new{2, 4}
s2 = Set.new{4, 10, 2}
print(s1 <= s2) --> true
print(s1 < s2) --> true
print(s1 >= s1) --> true
print(s1 > s1) --> false
print(s1 == s2 * s1) --> true
print(s1 ~= s2) --> true
Lua只提供__le
为了<=
,__lt
为了<
,__eq
为了=
这三个关系运算的”metamethod”。而例子中可以看到>=
,>
和~=
也都能正常进行运算。这是因为Lua对他们进行了相应的转换,
a >= b --> b <= a
a > b --> b < a
a ~= b --> not (a == b)
Q:如何定义库函数相关的”metamethods”?
A:有两个库函数相关的”metamethod”可以在程序中操作,__tostring
和__metatable
。
--[[ 定义"__tostring"可以"print()"以什么样的格式输出数据。
"print()"总是去调用"tostring()"以格式化它的输出。
在格式化的过程中,"tostring()"首先查看被格式化的对象是否有一个"metatable",
并且"metatable"中是否有"__tostring"(它必须是个函数)域。
如果有,则"tostring()"以被格式化对象作为参数,调用这个"metamethod"。
无论这个"metamethod"返回什么,都将作为"tostring()"的结果返回。]]
Set.mt.__tostring = Set.tostring
s1 = Set.new{10, 4, 5}
-- 定义了"__tostring"域,就无需使用"Set.print()"了,直接"print()"即可。
print(s1) --> {4, 5, 10}
t = {}
--[[ 对象被分配了带有"__tostring"域的"metatable",
才会被"Set.tostring()"格式化,其他的对象不受影响。]]
print(t) --> table: 0x1a241e0
-- 定义"__metatable"可以保护"metatable"不被随意的修改。
--[[ 在这段代码之后的所有的"getmetatable()"会返回你设定的字符串
(即"not your business"),
而所有的"setmetatable()"会报错。]]
Set.mt.__metatable = "not your business"
s1 = Set.new{}
print(getmetatable(s1)) --> not your business
setmetatable(s1, {}) --> cannot change protected metatable
附加:
1、Lua中的每一个”table”都可以有他自己的”metatable”(之后还会看到”userdata”也可以有自己的”metatables”)。任何”table”都可以是其他”table”的”metatable”。一组有关系的”table”可以共享一个公用的”metatable”(公用的”metatable”描述他们共有的行为,比如”add”)。一个”table”也可以有自己独有的”metatable”,那么就是描述他自己独有的行为。
2、当我们对两个集合做算数运算时,Lua会调用正确的”metamethod”。但如果一个集合与一个常量做算数运算呢?比如,
s = Set.new{1,2,3}
s = 8 + s
当Lua遇到这种情况时会做以下选择(同时,这种选择适用于所有算数运算符,关系运算符以及..
运算符的运算),
(1) 如果第一个值有”metatable”,并且其中有一个__add
方法,那么Lua以此作为整个算数运算的”metamethod”。
(2) 如果第一种方法行不通,那么Lua查看第二个值中是否有”metatable”,并且其中有一个__add
方法,如果有那么Lua以此作为整个算数运算的”metamethod”。
(3) 如果第二种方法也行不通,那么Lua报错。
所以上面的两行程序,Lua会调用”s”的”metatable”中的__add
方法,之后会报错:
bad argument #1 to ‘pairs’ (table expected, got number)
Lua不会在意这种混合的类型(集合与常量),但是我们的程序应该在意(否则就像这样报错了)。所以程序中要对传入的参数之类型做检查,
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
... -- same as before
end
3、”metatable”中还有个”metamethod”,”__concat”。如果设定了该”metamethod”,调用..
时会被触发,
-- 我们规定"s1 .. s2"仅是打印一句话,以证明调用了正确的"metamethod"。
function Set.concat(a, b)
local res = Set.new{"__concat"}
return res
end
Set.mt.__concat = Set.concat
Set.print(s1 .. s2) --> {__concat}