元表与元方法
- Lua 中每个值都有一套预定义的操作集,表示这个值可以有什么操作,这个操作集就是这个值的元表。
- 对一个值进行某种操作,lua 首先会查找它的元表看看有没有对应的字段,如果找到了字段,则调用该字段的值,也就是元方法,它是一个函数。
比如进行 a+b 操作时,先查找 a 或 b 的元表中有没有字段 __add,如果有这个字段,调用该字段对应的元方法,否则操作不合法。 - table 和 userdata 可以有各自独立的 metatable,其它数据类型共享其类型所属的单一 metatable
- 字符串默认有一个元表,其它数据类型默认没有元表,新创建的 table 也没有元表,可以使用 setmetatable 来为 table 设置元表。
print(getmetatable("hello")) -- 003E7050
print(getmetatable("lua")) -- 003E7050
print(getmetatable(12)) -- nil
print(getmetatable({})) -- nil
t = {}
print(getmetatable(t)) -- nil
setmetatable(t, {})
print(getmetatable(t)) --0048A700
算术类的元方法
- 在元表中,每种算术运算符都有对应的字段名。
加法(__add)、减法(__sub)、乘法(__mul)、除法(__div)、
取模(__mod)、取幂(__pow)、取相反数(__unm)、连接(__concat)
- 如果我们想给某个值赋予它本来没有的运算,先给它设置一个元表,然后修改元表,给对应操作的字段设置元方法
比如我们想给集合(table)赋予相加操作,表示两个集合求并集
-- 定义元表
local mt = {}
Set = {}
Set.new = function(l)
local set = {}
-- 设置元表
setmetatable(set, mt)
for _, v in ipairs(l) do
set[v] = true
end
return set
end
Set.union = function(a, b)
local res = {}
for k in pairs(a) do
res[k] = true
end
for k in pairs(b) do
res[k] = true
end
return res
end
Set.intersection = function(a, b)
local res = {}
for k in pairs(a) do
if b[k] then
res[k] = true
end
end
return res
end
Set.tostring = function(a)
local l = {}
for k in pairs(a) do
l[#l + 1] = k
end
return "{" .. table.concat(l, ',') .. "}"
end
Set.print = function(a)
print(Set.tostring(a))
end
-- 给元表添加元方法
mt.__add = Set.union
mt.__mul = Set.intersection
set1 = Set.new({1, 2, 3, 4, 5, 6})
set2 = Set.new({2, 4, 6, 8})
print(getmetatable(set1) == getmetatable(set2))
-- set3 = Set.union(set1, set2)
set3 = set1 + set2
Set.print(set3)
set3 = set1 * set2
Set.print(set3)
关系类的元方法
- 关系类的元方法有下面三个,其它的关系运算都可以转成这三种
等于(__eq)、小于(__lt)、小于等于(__le)
- 关于类的元方法不能应用于混合的类型,如果两个对象的元方法不同,对它们进行小于比较会出错,如果做等于比较,元方法不同直接返回 false,元方法相同才会进行比较
mt.__le = function(a, b)
for k, v 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
set1 = Set.new({2, 4, 6})
set2 = Set.new({1, 2, 3, 4, 5, 6})
print(set1 < set2) -- true
print(set1 > set2) -- false
print(set1 <= set2) -- true
print(set1 == set2) -- false
print(set1 == Set.new({2, 4, 6})) -- true
print(set1 == {2, 4, 6}) -- false
-- print(1 < "hello") -- error
其它库定义的元方法
- __tostring 字段定义了一个对象在使用 print 输出时的格式
- __metatable 字段保存了元表的值,如果修改了这个字段的值,元表就被保护起来,获取元的值将得到新设置的值,而设置元表的值将会出错
mt.__tostring = Set.tostring
-- 下面两条语句等价
print(set1)
print(Set.tostring(set1))
-- 保护元表
mt.__metatable = "不能访问"
s = Set.new({})
print(getmetatable(s))
-- setmetatable(s, {}) -- error
table 访问的元方法
__index 元方法
- 当访问一个 table 不存在的字段时,解释器会首先查找 table 有没有 __index 元方法,如果有则调用 __index 元方法,如果没有才返回 nil
- __index 的值也可以是一个 table
- 使用 rawget 可以对 table 进行原始访问,即不判断 __index 字段
Window = {}
-- 定义原型
Window.propotype = {x = 10, y = 10, width = 100, height = 100}
-- 定义元表
Window.mt = {}
-- __index 的值可以是函数
Window.mt.__index = function(t, k)
return Window.propotype[k]
end
-- __index 的值也可以是 table
-- Window.mt.__index = Window.propotype
-- 构造函数
Window.new = function(o)
setmetatable(o, Window.mt)
return o
end
win = Window.new({x = 0, y = 0})
print(win.x, win.width) -- 0 100
print(win.x, rawget(win, width)) -- 0 nil
__newindex 元方法
- 当设置一个 table 不存在的字段的值时,解释器会首先查找 table 有没有 __newindex 字段,如果有则调用对应的元方法,如果没有才给 table 设置新字段
- __newindex 也可以是函数或 table
- 使用 rawset 可绕过 __newindex 直接对 table 赋值
Window.mt.__newindex = function(t, k, v)
Window.propotype[k] = v
end
win["color"] = "red"
print(win.color, rawget(win, "color")) -- red nil
rawset(win, "weight", 8)
print(win.weight, rawget(win, "weight")) -- 8 8
设置 table 的默认值
- 如果没有为 table 元表的 __index 字段赋值,那么 table 的字段默认值就是 nil;可以通过 __index 修改 table 的默认值
给每个 table 新建一个元表
使用这种方式很简单,只需要让 __index 元方法固定返回一个值即可;但这种方式必须为每个 table 新创建一个元表,开销比较大
setDefault = function(t, value)
local mt = {__index = function()
return value
end}
setmetatable(t, mt)
end
local t = {}
print(t.x) -- nil
setDefault(t, 10)
print(t["x"]) -- 10
让不同的 table 共享一个元表
由于默认值是与元方法 __index 关联在一起的,因此无法直接在元方法中保存和返回多个默认值;可以将默认值保存在各个 table 本身,然后使用一个 key 在元方法中访问
- 如果不考虑名字冲突的话,可使用 _ 来表示这个 key
-- 共同的元表
local mt = {__index = function(t)
return t.___
end}
setDefault = function(t, value)
-- 默认值保存在 table 本身
t.___ = value
setmetatable(t, mt)
end
- 如果担心默认值的 key 与 table 的其它字段有名字冲突,使用一个新 table 来当作默认值的 key 以确保名字唯一
local key = {}
mt = {__index = function(t)
return t[key]
end}
setDefault = function(t, value)
-- 默认值保存在 table 本身
t[key] = value
setmetatable(t, mt)
end
跟踪 table 的访问
只有一个 table 为空表时,才能保证每次访问 table 的字段才能定位到 __index 或 __newindex;因此可以创建一个空的 table 作为原 table 的代理,然后在这个空的 table 的 __index 和 __newindex 中访问原 table 的字段
给每个 table 新建一个代理 table
setAgent = function(t)
local agent = {}
local mt = {__index = function(_, k)
print("正在访问元素" .. tostring(k))
local value = t[k]
if value == nil then
print("元素为空,返回默认值")
value = 10
end
return value
end, __newindex = function(_, k, v)
print("给元素" .. tostring(k) .. "赋值为" .. tostring(v))
t[k] = v
end}
setmetatable(agent, mt)
return agent
end
local t = {x = 10, y = 20}
t = setAgent(t)
print(t.x)
print(t.w)
t.w = 100
print(t.w)
通过函数 setAgent 给表 t 设置了一个代理空表,然后把这个空表赋给 t,之后对表 t 的操作都是通过这个空表来操作的,通过这个空表的 __index 和 __newindex 来操作原来的表
让不同的代理 table 共享一个元表
使用跟默认值一样的方式,把原来的 table 保存到各个代理本身,然后元表通过 key 来访问
local index = {}
local mt = {__index = function(t, k)
print("正在访问元素" .. tostring(k))
local value = t[index][k]
if value == nil then
print("元素为空,返回默认值")
value = 10
end
return value
end, __newindex = function(t, k, v)
print("给元素" .. tostring(k) .. "赋值为" .. tostring(v))
t[index][k] = v
end}
setAgent = function(t)
local agent = {}
agent[index] = t
setmetatable(agent, mt)
return agent
end
只读的 table
通过代理很容易实现只读的 table,只需要在设置值的时候返回一个错误即可
readOnly = function(t)
local mt = {__index = t, __newindex = function(t, k, v)
error("the table is read only!", 2)
end}
local agent = {}
setmetatable(agent, mt)
return agent
end
t = readOnly(t)
t.x = 10 -- error