09-元表
在Lua语言中,元表(Metatable)定义拥有它的实例的行为。例如,平常无法计算两个表相加,可以通过定义元表内的元方法(Metafunction)字段__add告诉Lua如何计算这两个表的加法。Lua语言中的每一个值都可以有元表:对于表和用户数据(userdata)类型的值,可以拥有各自独立的元表;对于其他类型的值(如字符串)则共享类型所属的同一个元表。
获取&设置元表
Lua语言在创建新表时不附带创建它所属的元表,可以通过getmetatable()函数查看该表所属的元表:
t = {}
print(getmetatable(t)) --> nil
可以使用函数setmetatable()来设置/修改任意表的元表:
tMeta = {}
setmetatable(t, tMeta)
print(getmetatable(t)) --> table: 0000000000dca930
在Lua语言中只能为表设置元表,如果要为其他类型的值设置元表,则必须通过C代码或调试库完成。
元方法
算术运算相关
每种算术运算符都有一个对应的元方法:加法__add,减法__sub,乘法__mul,除法__div,向下取整除法floor__idiv,取负数__unm,取模__mod和幂运算__pow。
在Lua5.3加入位运算后,位运算也有各自对应的元方法:按位与__band,按位或__bor,按位异或__bxor,按位取反__bnot,左移__shl和右移__shr。
此外,连接运算符..也有对应的元方法__concat。
下例为加法元方法__add在表加法计算中的应用:
local t = { value = 5 }
local t2 = { value = 10 }
local metaTable = {
__add = function (t1, t2)
return t1.value + t2.value
end
}
setmetatable(t, metaTable)
-- setmetatable(t2, metaTable)
print(t + t2) --> 15
对于给定统一元表的表的计算,使用哪个元表是确定的。但在上例中t2并没有被赋予元表也能正确计算,这就涉及到Lua查找相关元方法的逻辑了:
- 如果第一个值有元表,且元表中存在所需的元方法,那么Lua语言就使用这个元方法,与第二个值无关;
- 如果第二个值有元表,且元表中存在所需的元方法,那么Lua语言就使用这个元方法;
- 经过上述步骤仍然找不到所需的元方法,抛出异常;
关系运算相关
只有3种关系运算符拥有对应的元方法:等于__eq,小于__lt和小于等于__le,其他三个关系运算符没有单独的元方法,Lua会根据已有的关系运算进行推断。
需要注意的是,如果两个对象的类型不同,那么相等比较操作不会调用任何元方法而是直接返回false。
表相关
Lua语言也提供了一些修改表行为的元方法。
__index
当访问表中不存在的字段时,Lua会先在元表中查找元方法__index,如果存在就根据元方法的逻辑返回结果,如果不存在就返回nil。虽然__index被称作元方法,但也能用表赋值:
local prototype = { x = 10, y = 0 }
local mt = {}
mt.__index = prototype
--[[ 可以用函数或表赋值
mt.__index = function (table, key)
return prototype[key]
end
--]]
local function new(t)
setmetatable(t, mt)
return t
end
local w = new({})
print(w.x, w.y, w.z) --> 10 0 nil
此外,可以通过rawget(table, index)函数在访问表时不调用__index元方法。
__newindex
当对一个表中不存在的索引赋值时,Lua会先在元表中查找元方法__newindex,如果该元方法存在:
- 作为函数存在:调用该函数,不执行赋值;
- 作为表存在:在
__newindex指向的表中进行赋值,而不是在原来的表中;
此外,可以通过rawset(table, key, value)函数在给表赋值时不调用__newindex元方法。
__call
尝试将表作为函数调用时,Lua会优先检查元方法字段__call,如果此处被定义就会调用相应函数:
local t = { name = "Wow" }
local metaTable = {
__call = function ()
print("Calling t.")
end
}
setmetatable(t, metaTable)
t() --> Calling t.
如果函数定义中有参数,需注意的是第一个参数为表本身。
库定义相关
除了核心Lua语言,一些程序库也会在元表中定义和使用它们自己的字段。
__tostring
对于表来说,print()函数会调用它的tostring()函数进行格式化输出。而tostring()函数首先会检查元方法字段__tostring是否被赋值,如果有的话就会将表对象作为参数传给该函数,最后将元方法的返回值作为它的返回值。
local t = { name = "Wow" }
local metaTable = {
__tostring = function (s)
return "Table's name: " .. s.name
end
}
print(t) --> table: 00000000001da470
setmetatable(t, metaTable)
print(t) --> Table's name: Wow
__metatable
为了保护对象的元表不被修改,就要阻止用户既不能通过getmetatable()看到元表,也不能通过setmetatable()修改元表。Lua语言给元表提供了__metatable字段,使用getmetatable()后会返回该字段的值,使用setmetatable()则会报错:
local t = {}
local metaTable = { __metatable = "从我家里出去!" }
setmetatable(t, metaTable)
print(getmetatable(t)) --> 从我家里出去!
setmetatable(t, {}) --> 报错:cannot change a protected metatable
__pairs
从Lua5.2开始,函数pairs()也有了对应的元方法__pairs,因此可以修改表被遍历的方式以及为非表对象增加遍历行为。当一个对象拥有__pairs元方法时,pairs()函数会调用这个元方法完成遍历。
参考资料
- 《Lua程序设计 (第4版)》
- 【唐老狮】Unity热更新之Lua语法