跳到主要内容
信息
· 文章中可能会出现一些错误,希望大佬们可以在评论区指出;

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查找相关元方法的逻辑了:

  1. 如果第一个值有元表,且元表中存在所需的元方法,那么Lua语言就使用这个元方法,与第二个值无关;
  2. 如果第二个值有元表,且元表中存在所需的元方法,那么Lua语言就使用这个元方法;
  3. 经过上述步骤仍然找不到所需的元方法,抛出异常;

关系运算相关​

只有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()函数会调用这个元方法完成遍历。

参考资料​