目录
1. 元表是一类特殊的表,能修改一个值在面对一个未知操作时的行为。
2. Lua语言中的每一个值都可以有元表。其中,每一个表和用户数据类型都具有各自独立的元表,而其他类型的值共享对应类型所属的同一元表
3. 在Lua语言中,只能为表设置元表;如果要给其他类型的值设置元表,则必须通过C代码或调试库完成。
4. 字符串标准库为所有的字符串都设置了同一个元表,而其他类型默认情况下没有元表 (字符串默认有元表,其他类型没有)
第二步: 假设想使用加法操作符来计算两个集合的并集,那么可以让所有集合的表共享一个元表,在通过定义元方法__add 实现
5.1 __index元方法:用于表的查询,可以应用于继承、创建具有默认值的表等
5.1.1 原理:当访问一个表中不存在的字段时会得到 nil ,实际上还会 引发解释器查找一个名为 __index的元方法,如果没有这个元方法,结果就返回 nil, 如果有则使用元方法提供的值
5.1.4 当希望访问一个表时不调用__index 元方法,可以使用函数 rawget. 调用rawget(t, i) 会对表t 进行原始访问(不考虑元表,简单对表进行访问)
5.2 __newindex元方法:用于表的更新,可用作 实现只读的表
5.2.1 原理:当对一个表中不存在的索引赋值时,解释器会查找__newindex元方法, 如果元方法存在,那么解释器就调用它而不进行赋值
5.2.2 调用rawset(t, k, v) 等价于 t[k] = v ,绕过元方法
一. 元表和元方法的介绍
通常,Lua语言中的每种类型的值都有一套操作集合。例如,我们可以把数字相加,可以连接字符串等。但是,我们无法将两个表相加,无法对函数作比较,除非使用元表。
1. 元表是一类特殊的表,能修改一个值在面对一个未知操作时的行为。
例如,表a和表b 试图相加时,Lua会检查两表之一是否存在元表且 该元表中是否有_add字段,存在该字段,就调用该字段对应的值(即 所谓的元方法 metamethod)
2. Lua语言中的每一个值都可以有元表。其中,每一个表和用户数据类型都具有各自独立的元表,而其他类型的值共享对应类型所属的同一元表
t = {}
print(getmetatable(t)) -- > nil
t1 = {}
setmetatable(t, t1) -- 将t1设置成t的元表
print(getmetatable(t) == t1) --> true
3. 在Lua语言中,只能为表设置元表;如果要给其他类型的值设置元表,则必须通过C代码或调试库完成。
4. 字符串标准库为所有的字符串都设置了同一个元表,而其他类型默认情况下没有元表 (字符串默认有元表,其他类型没有)
print(getmetatable("hi")) -- table:0x807772e0
print(getmetatable("xuxu")) -- table:0x807772e0
print(getmetatable(10)) -- nil
print(getmetatable(print)) -- nil
二 算术运算相关的元方法
2.1 Demo: 一个用于集合的简单模块
local Set = {}
-- 使用指定的列表创建一个新的集合
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] -- 如 b[k]不存在,则b[k]为nil, res[k] = nil 意味着删除 该元素,所以 res[k]只会保存 true
end
return res
end
function Set.tostring(set)
local l = {}
for e in pairs(set) do
l[#l + 1] = tostring(e)
end
return "{" .. table.concat(l, ", ") .. "}"
end
return Set
第二步: 假设想使用加法操作符来计算两个集合的并集,那么可以让所有集合的表共享一个元表,在通过定义元方法__add 实现
local mt = {} -- 集合的元表
function Set.new(l)
local set = {}
setmetatable(set, mt)
for _, v in ipairs(l) do set[v] = true end
return set
end
这样,所有由Set.new创建的集合都具有一个相同的元表了
s1 = Set.new{10,20,30,50}
s2 = Set.new{30, 1}
print(getmetatable(s1)) -- table:0x00672B60
print(getmetatable(s2)) -- table:0x00672B60
第三步: 在元表中加入元方法 __add
mt.__add = Set.union
s3 = s1 + s2
print(Set.tostring(s3)) --> {1, 10, 20, 30, 50}
类似地,也可以使用乘法运算符计算集合的交集
mt._mul = Set.intersection
print(Set.tostring(s1 + s2)*s1) --> {10, 20, 30, 50}
2.2 算术运算符的元方法
元方法 | 含义 |
__add | 加法 |
__mul | 乘法 |
__sub | 减法 |
__div | 除法 |
__idiv | floor除法 |
__unm | 负数 |
__mod | 取模 |
__pow | 幂运算 |
__band | 按位与 |
__bor | 按位或 |
__bxor | 按位异或 |
__bnot | 按位取反 |
__shl | 向左移位 |
__shr | 向右移位 |
3. 关系运算相关的元方法
元方法 | 含义 |
__eq | 等于 |
__lt | 小于 |
__le | 小于等于 |
3.1 其他三个关系运算符 不等于、大于、大于等于的实现
由于这三个没有单独的元方法,可通过上面的转换得出
不等于: a ~= b 装换为 not(a == b)
大于: a > b 装换为 b < a
大于等于:a >= b 装换为 b
-- 实现小于等于,子集关系
mt.__le = function (a, b)
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
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
4. 库相关的元方法
程序库在元表中定义和使用它们自己的字段也是一种常用的实践
例如:
print({}) -- table:0x8062ac0
-- 输出一个字符串
函数print 总是调用 函数tostring 来进行格式化输出,不过,当对值进行格式化时,函数tostring 会首先检查值是否有一个元方法 __tostring, 如果有,函数tostring 就调用这个元方法,将对象作为参数传给 该函数,然后把元方法的返回值作为函数tostring 的返回值
在之前集合的示例中,已经定义了一个将集合表示为字符串的函数,只需要在元表中设置 __tostring 字段:
mt.__tostring = Set.tostring
s1 = Set.new{10, 4, 5}
print(s1) -- {4, 5, 10}
5. 表相关的元方法
5.1 __index元方法:用于表的查询,可以应用于继承、创建具有默认值的表等
Demo: 用继承的概念,创建一个窗口对象
prototype = {x = 0, y = 0, width = 100, height = 100} -- 创建默认值原型表
local mt = {} --创建一个元表
-- 声明构造函数
function new(o)
setmetatable(o, mt)
return o
end
--定义元方法
mt.__index = function (_, key)
return prototype[key]
end
w = new{x=10, y=20}
print(w.width) -- 100
5.1.1 原理:当访问一个表中不存在的字段时会得到 nil ,实际上还会 引发解释器查找一个名为 __index的元方法,如果没有这个元方法,结果就返回 nil, 如果有则使用元方法提供的值
5.1.2 实现方式:在上例中,Lua语言发现w中没有对应的字段“width”,但却有一个带有 __index元方法的元表,Lua语言会以 w(表)和 "width" (不存在的键)为参数调用这个元方法,元方法随后会用这个键检索原型返回结果
5.1.3 元方法 __index 不一定必须是函数,也可以是一个表,当元方法是函数时,Lua语言会以表和不存在的键为参数调用该函数;当元方法是一个表时,Lua语言就访问这个表
mt.__index = prototype
5.1.4 当希望访问一个表时不调用__index 元方法,可以使用函数 rawget. 调用rawget(t, i) 会对表t 进行原始访问(不考虑元表,简单对表进行访问)
5.2 __newindex元方法:用于表的更新,可用作 实现只读的表
5.2.1 原理:当对一个表中不存在的索引赋值时,解释器会查找__newindex元方法, 如果元方法存在,那么解释器就调用它而不进行赋值
5.2.2 调用rawset(t, k, v) 等价于 t[k] = v ,绕过元方法
5.3 具有默认值的表
一个普通表中所有字段的默认值都是 nil。通过元表,可以很容易修改这个默认值:
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)
setDefault(tab, 0)
print(tab.x, tab.z) -- 10, 0
函数setDefault 为所有需要默认值的表创建了一个新的闭包和一个新的元表,如果此时需要很多默认值的表,那开销就会很大,可以把 创建元表的方法放到函数外部,只创建一遍
local mt = {__index = function(t) return t.__ end}
function setDefault()
t.___ = d
setmetatable(t, mt)
end
这样,每个表中的 ___字段 都会作为默认值返回
如果 担心命名冲突,那需要把___字段替换成一个唯一的值:可以用空表作为 表的键,这样,key值就是唯一的
local key = {}
local mt = {__index = function (t) return t[key] end}
function setDefault()
t[key] = d
setmetatable(t, mt)
end
5.4 跟踪对表的访问
function track(t)
local proxy = {} -- 为代理创建元表
local mt = {
__index = function (_, k)
print("*access to element " .. tostring(k))
return t[k] -- 访问原来的表
end,
__newindex = function(_, k, v)
print("*update of element " .. tostring(k) .. " to" .. tostring(v))
t[k] = v
end,
__pairs = function ()
return function (_, k)
local nextkey, nextvalue = next(t, k)
if nextkey ~= nil then
print("traversing element " .. tostring(nextkey))
end
return nextkey, nextvalue
end
end,
__len = function () return #t end
}
setmetatable(proxy, mt)
return proxy
end
t = {}
t = track(t)
t[2] = "hello"
-- *update of element 2 to hello
print(t[2])
-- *access to element 2
-- hello
元方法 __ index 和__newindex 按照我们设计的规则跟踪每一个访问并将其重定向到原来的表中 。元方法__pairs 使得我们能够像遍历原来的表一样遍历代理,从而跟踪所有的访问。
最后,元方法__ len 通过代理实现了长度操作符:
t = track({10, 20})
print(#t) --2
for k, v in pairs(t) do print(k, v) end
-- *traversing element 1
-- 1 10
-- traversing element 2
-- 2 20
5.5 只读的表
可以使用代理(空表做代理)的概念来实现只读的表,需要跟踪表的更新操作并抛出异常即可:
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]) --Sunday
days[2] = "Noday"
-- stdin:1: attempt to update a read-only table