Lua-元方法和元表基础
简介
通常,Lua中每种类型的值都有一套可预见的操作集合。例如,可以将数字相加,将字符串连接,还可以在表中插入键值对等。但是我们无法直接将两个表相加,无法对函数作比较。
元表(metatable
)可以修改一个值在面对一个未知操作时的行为。例如,对于两个表a和b,可以通过元表定义a+b
。当Lua试图将两个表相加时,会先检查两者之一是否有元表且该元表中是否有__add
字段。如果找到了该字段,就调用它对应的值,即元方法(metamethod
)进行计算。
可以通过setmetatable(tbl, t)
和getmetatable(tbl)
设置、获取表的元表
Lua中只能为表设置元表,字符串标准库为所有的字符串都设置了同一个元表。为其他类型的值设置元表要修改C代码支持。
基础元方法使用举例
local Set = {}
function Set.new(t)
local o = {}
setmetatable(o, Set)
for _,v in ipairs(t) do
o[v] = true
end
return o
end
-- 算术运算符元方法+,实现两个Set相加
function Set.__add(a, b)
local result = Set.new({})
for k in pairs(a) do result[k] = true end
for k in pairs(b) do result[k] = true end
return result
end
-- 关系运算符元方法==,检测两个Set是否相等
function Set.__eq(a, b)
for k in pairs(a) do
if not b[k] then return false end
end
for k in pairs(b) do
if not a[k] then return false end
end
return true
end
-- 库元方法,实现Set格式化输出
function Set.__tostring(set)
local tbl = {}
for k in pairs(set) do
tbl[#tbl+1] = k
end
return "{"..table.concat(tbl,",").."}"
end
-----------------
local set_1 = Set.new({1,25,36,1,40})
local set_2 = Set.new({2,2,25,3})
print(set_1 + set_2) -- 打印: {1,2,3,40,25,36}
set_1 = Set.new({1,1,2,4,4})
set_2 = Set.new({1,2,4})
print(set_1 == set_2) -- 打印: true
正常情况下,我们可以继续为set_1
设置元表,假如我们想限制这种操作,保护我们定义的Set
,可以在元表中设置__metatable
字段,如下所示:
-- 其他代码同上
Set.__metatable = "禁止修改集合的元表"
print(getmetatable(set_1)) -- 打印 "禁止修改集合的元表"
setmetatable(set_1,{}) -- 将会报错,提示 cannot change a protected metatable
表相关的元方法
以上算术运算符、关系运算符和库对应的元方法,都没有改变语言的正常行为。Lua还提供了一些方法来访问和修改表中不存在的字段。
__index 元方法
正常情况下,访问表中不存在的字段会得到nil
。实际的流程是,这些访问会引发解释器查找一个名为__index
的元方法,如果没有这个元方法,结果就是nil
;否则由这个元方法来提供最终结果。
local property = {width = 250, height = 1000}
local Shape = {}
function Shape.new(o)
setmetatable(o, Shape)
return o
end
function Shape.__index(param, key)
return property[key]
end
local s = Shape.new({name = "圆", ret = true})
-- s有name字段,直接返回,s没有width字段,会去找元表中__index的返回结果,最终得到 250
print(s.name, s.width) -- 打印 圆 250
利用__index
可以实现继承,当我们希望获得一个表原始的字段,即不调用__index
方法,可以使用rawget(t, index)
,例如,对以上代码做修改:
-- 第一个返回__index结果,第二个返回原始table的字段,不存在width所以为nil
print(s.width, rawget(s,"width")) -- 打印 250 nil
当然,进行原始访问并不会加快代码的运行,只是有时候确实有这种需求。
__newindex 元方法
__newindex
与__index
类似,不同之处在于前者用于表的更新而后者用于表的查询。当对一个表中不存在的索引赋值时,
解释器就会查找__newindex
元方法,如果存在,解释器就调用它而不会执行赋值。用法如下:
local t = {
name = "Name",
age = 12
}
local mt = {
__newindex = function(param, key)
print("禁止赋值【"..key.."】")
end
}
setmetatable(t, mt)
t.extra = 25 -- 打印 禁止赋值【extra】
同样我们可以调用rawset(t,k,v)
绕过__newindex
而直接赋值
组合使用元方法__index
和__newindex
可以实现一些强大的结构,例如只读的表、具有默认值的表和继承。
创建具有默认值的表
正常一个空表所有字段默认值都是nil,通过元表可以设置默认值。
-- 保证key的唯一性,创建一个新表作为默认值的key
local _key = {}
local mt = {__index = function(t) return t[_key] end}
local function SetDefault(t, v)
t[_key] = v
setmetatable(t, mt)
end
local t = {a = 15, b = 20}
SetDefault(t, -1)
print(t.c) -- 打印 -1
设置只读的表
function SetReadOnly(t)
local proxy = {}
local mt = {
__index = t;
__newindex = function()
error("table is read only", 2)
end
}
setmetatable(proxy, mt)
return proxy
end