元表简介
- 元表: Lua 中的每个值都可以有一个 元表。 这个 元表 其实就是一个普通的 Lua 表, 它用于定义原始值在特定操作下的行为。 如果你想改变一个值在特定操作下的行为,你可以在它的元表中设置对应域。 例如,当你对非数字值做加操作时, Lua 会检查该值的元表中的 "__add" 域下的函数。
- 元表主要用于定义表的行为:例如如何处理索引不存在的情况、如何进行相关运算等。元表提供了一些特殊的字段(元方法),比 如 __index、__newindex、__add、__tostring 等。
- 元表可以让一个基础的自定义数据类型 实现 内建行为(内建函数、运算符等)
- 元表可以实现一个类
- 元表可以看作一个普通表的 方法类,类似于C 中的纯虚类
- 如何设置元表? 可以通过 setmetatable 函数给一个表设置元表,getmetatable 来获取任何值的元表
local mt = {} -- 创建元表
local t = {a=1, b=2} -- 创建主体表
-- 尝试获取元表
local mt_got = getmetatable(t)
if mt_got == nil then
print("The table does not have a metatable.")
else
print("The table has a metatable.")
end
--输出 The table does not have a metatable.
setmetatable(t, mt) -- 设置主体表的元表
local t = {a=1, b=2}
-- 尝试获取元表
local mt_got = getmetatable(t)
if mt_got == nil then
print("The table does not have a metatable.")
else
print("The table has a metatable.")
end
--输出 The table has a metatable.
元方法
讲到元方法就必须得提__index
1.__index
- 索引key不存在时触发。 当 table 不是表或是表 table 中不存在 key 这个键时,这个事件被触发。此时,会读出 table 相应的元方法。这个事件的元方法可以是一个函数也可以是一张表。
- 如果它是一个函数,则以 table 和 key 作为参数调用它。
- 如果它是一张表,最终的结果就是以 key 取索引这张表的结果。这个索引过程是走常规的流程,而不是直接索引,所以这次索引有可能引发另一次元方法的调用。
- 总而言之:__index 是一个特殊的元方法,当尝试访问一个表中不存在的键时,Lua 会调用这个方法。这个方法可以用来提供默认值或者实现lua类继承行为。
1.函数调用
函数调用会返回函数的返回值(table表和key索引值会作为参数传递进去)
代码语言:lua复制-- MetatableTest.lua
local mt = {
__index = function (t, key)
print("正在寻找 "..key.."是否在元表里")
if key == 'specialKey' then
return "关键值"
else
return "未知值"
end
end
}
local t = {a=1, b=2}
setmetatable(t, mt)
print("t.specialKey:"..t.specialKey) -- 输出:正在寻找 "..specialKey.."是否在元表里
-- t.specialKey:关键值
print("t.C:"..t.C) -- 输出: 正在寻找 C是否在元表里
-- t.C:未知值
print("t.a:"..t.a) -- 输出: t.a:1
2.表调用
- 表调用Lua查找元素的规则如下:
- 在表中查找,找到则返回,找不到则继续
- 判断是否有元表,没有返回nil,有则继续
- 判断元表有无__index方法,如果该方法为nil,则返回nil;如果是一个表,则重复1-3;
- 如果是一个函数,则返回函数的返回值(table和key会作为参数传递进去)
-- MetatableTest.lua
local mt = {
__index = { c = 3, d = 4 } -- 设置 __index 为一个包含键值对的新表
}
local t = {a=1, b=2,'a','b','c'}
setmetatable(t, mt)
-- 检查元表设置是否成功
print(getmetatable(t)) -- 应该输出: table: 地址
print(t.a) -- 输出: 1
print(table.concat(t, '|')) -- 输出 a|b|c
print(t.c) -- 输出: 3
print(t.d) -- 输出: 4
print(t.e) -- 输出: nil
2.__newindex
- __newindex 元方法允许你自定义对表进行赋值时的行为。
- 一旦有了 "newindex" 元方法, Lua 就不再做最初的赋值操作。
- 当尝试向表中添加一个新的键或更新一个已存在的键时,Lua 会调用这个方法。这个方法可以用来拦截对表的修改操作,从而实现只读表或者其他自定义的行为。
- 如果有必要,在调用__newindex元方法内部或者外部想绕过__newindex时可以调用 rawset 来做赋值
- 这是因为Lua或者C/C 层面直接调用rawset设置值时是不会触发__newindex元方法的
-- MetatableTest.lua
local mt = {
__index = { c = 3, d = 4 }, -- 设置 __index 为一个包含键值对的新表
__newindex = function(t, key, value)
print("Setting key ", key, " to value ", value)
rawset(t, key, value) -- 使用 rawset 直接设置到表中
end
}
local t = {a=1, b=2, 'a', 'b', 'c'}
setmetatable(t, mt)
-- 检查元表设置是否成功
print(getmetatable(t)) -- 应该输出: table: 地址
print(table.concat(t, '|')) -- 输出: a|b|c
print(t.c) -- 应该输出: 3
print(t.d) -- 应该输出: 4
print(t.e) -- 应该输出: nil
-- 使用 __newindex
t.newkey = "newvalue" -- 触发 __newindex
-- 使用 rawset
rawset(t, 'anotherkey', "anothervalue") -- 不触发 __newindex
-- 显示所有键值对
for k, v in pairs(t) do
print(k..'|'..v)
end
newindex只读表的实现
代码语言:lua复制local readonl1yTable = setmetatable({}, {
__newindex = function(tbl, key, value)
error("Attempt to modify a read-only table")
end
})
-- 尝试修改只读表
readonlyTable.someKey = "someValue" -- 这里会抛出错误
在这个例子中,当我们尝试向 readonlyTable 添加一个新键或更新一个已存在的键时,Lua 会调用元表中的 __newindex 方法。由于我们定义了这个方法来抛出一个错误,因此任何对 readonlyTable 的修改都会失败,并抛出一个错误信息。
3.运算符类元方法
元表中有一些类似于CPP重载运算符的操作
当调用相应的运算符时会根据对应模式域触发相应的事件
模式 | 描述 |
---|---|
add | 对应的运算符 ' ' |
sub | 对应的运算符 '-' |
mul | 对应的运算符 '*' |
div | 对应的运算符 '/' |
mod | 对应的运算符 '%' |
unm | 对应的运算符 '-' |
concat | 对应的运算符 '..' |
eq | 对应的运算符 '==' |
lt | 对应的运算符 '<' |
le | 对应的运算符 '<=' |
以__add为例
代码语言:lua复制-- MetatableTest.lua
-- 因为table_maxn在lua5.2以后废弃了所以需要自己实现
-- 自定义计算表中最大键值函数 table_maxn,即返回表最大键值
function table_maxn(t)
local mn = 0
for k, _ in pairs(t) do
if type(k) == "number" and k > mn then
mn = k
end
end
return mn
end
-- __add 两表元素相加操作
print("--------------------------------------------------")
local mytable1 = setmetatable({1, 2, 3}, {
__add = function(mytable, newtable)
local max_key_mytable = table_maxn(mytable)
for i = 1, table_maxn(newtable) do
table.insert(mytable, max_key_mytable i, newtable[i])
end
return mytable
end
})
local secondtable1 = {4, 5, 6}
mytable1 = mytable1 secondtable1
print("After adding tables:")
for k, v in ipairs(mytable1) do
print(k.."|"..v)
end
-- __add 表里的两个数值相加操作
print("--------------------------------------------------")
local mytable2 = setmetatable({1, 2, 3}, {
__add = function(mytable, newtable)
local max_len = math.min(#mytable, #newtable)
for i = 1, max_len do
mytable[i] = mytable[i] newtable[i]
end
return mytable
end
})
local secondtable2 = {4, 5, 6}
mytable2 = mytable2 secondtable2
print("After adding values:")
for k, v in ipairs(mytable2) do
print(k.."|"..v)
end
4.__tostring
__tostring 元方法用于控制如何将一个对象转换为字符串。当将一个对象转换成字符串时(例如,使用 tostring 函数或在 print 函数中打印一个对象),如果对象的元表中定义了 __tostring 元方法,那么这个元方法将被调用。
代码语言:lua复制-- MetatableTest.lua
-- __tostring 函数
print("--------------------------------------------------")
mytable = setmetatable({ 10, 20, 30 }, {
__tostring = function(mytable)
sum = 0
for k, v in pairs(mytable) do
sum = sum v
end
return "表所有元素的和为 " .. sum
end
})
print(mytable) -- 输出表所有元素和为 60
5.__call
__call 元方法用于控制如何将一个对象当作函数来调用。当你尝试将一个对象当作函数调用时(例如,使用 obj(arg1, arg2) 的形式),如果对象的元表中定义了 __call 元方法,那么这个元方法将被调用。调用这个元方法时, func 作为第一个参数传入,原来调用的参数(args)后依次排在后面。
代码语言:lua复制-- MetatableTest.lua
-- 自定义计算表中最大键值函数 table_maxn,即返回表最大键值
function table_maxn(t)
local mn = 0
for k, _ in pairs(t) do
if type(k) == "number" and k > mn then
mn = k
end
end
return mn
end
-- 定义元方法 __call
local mytable = setmetatable({10}, {
__call = function(mytable, newtable)
local sum = 0
for i = 1, table_maxn(mytable) do
sum = sum mytable[i]
end
for i = 1, table_maxn(newtable) do
sum = sum newtable[i]
end
return "两个表所有元素和为"..sum
end
})
local newtable = {10, 20, 30}
print(mytable(newtable)) --输出 "两个表所有元素和为70"
总结
元表定义了值在某些特定操作下的行为,根据行为域执行特定的元方法。
元表和元方法是Lua语言中强大的工具,能够帮助开发者实现更复杂的功能,并且提高代码的灵活性和可维护性。理解并正确使用元表可以使Lua程序更加健壮和高效。然而,过度使用或不恰当的使用元表可能会导致难以调试的问题,因此使用时需谨慎。
参考文章:
Lua 5.3 参考手册