一、什么是元表
Lua 中的 table 使用起来有点像c++中的 map 或者 unordered_map ,都是通过对应的key 获取对应的value。如果访问了表中不存在的key时,就会触发Lua的一种机制,Lua也正是凭借这个机制可以用来模拟类似“继承”的行为,具体可以参考上一篇文章Lua中self 、自索引及其面向对象应用代码示例。
元表用来定义一个table在面对未知操作时候的行为,比如,对于a b 两个table,是无法进行相加操作 即:a+b 会报错。
比如:
a, b = {1, 2, 3}, {10, 20} c = a + b print(c) -- 输出的是a的地址
定义了两张表 a 和 b,当Lua执行到 a + b 时就会报错,因为Lua不知道如何将两个table相加。
元表两个重要的方法
setmetatable(tab, meta_tab) --设置tab的元表为meta_tab getmetatable(tab) -- 获取tab的元表
后续的例子中,会再详细讲解两个方法的用法。
二、元方法
2.1 通过元表实现两个table的相加
定义一个元表meta_ta, 并且实现 __add 方法,然后通过 setmetatable(a, meta_ta) 将meta_ta设置为a的元表 。此时,当Lua 执行到 a+b ,会先检查a 或 b 有没有元表,如果有元表且元表中实现了 __add ,就调用该key对应的值,__add对应的值(函数或者table)就是“元方法”。如果 a 和 b 都有元表会执行a的元表。
a, b = {1, 2, 3}, {10, 20} -- print(a + b) meta_ta = { __add = function(t1, t2) for k, v in pairs(t2) do -- 将表t2中的元素追加到t1中 table.insert(t1, v) end return t1 end } setmetatable(a, meta_ta) c = a + b print(c) -- 输出的是a的地址 for k, v in pairs(c) do print(k, v) end
运行结果:
运算符相关的元方法除了 __add 还有 减法 乘法 除法 取模 大于 小于 等等
2.2 Table常用的元方法
__index 元方法
当我们访问表中一个不存在的字段时,得到的结果会是nil。但时,如果为该表设置了元表,访问的时候解释器就会去元表查找一个名为 __index 的元方法,如果没有这个元方法,返回nil,否则,由这个元方法来提供最终结果。
-- 定义表t2 t2 = {id = 1, name = "panda", age = 25} -- 1. 显式指定meta_t2 为 t2 的元表 -- meta_t2 = {} -- setmetatable(t2, meta_t2) -- meta_t2.__index = { addr = "beijing"} -- 2. 隐式指定元表(匿名元表) setmetatable(t2, { -- __index 是个表 -- __index = { addr = "beijing"} -- __index 是function __index = function(t, k) print(t, k) -- 输出 表t的地址 和 key t[k] = "深圳" -- 为 表t 添加kv return "北京" end, }) print(t2.name) -- 输出 panda print(t2.addr) -- 输出biao t2 的地址 和 key: addr 及返回值 北京 print(t2.addr) -- 因为已经成功添加kv 直接输出
运行结果:
分析:
- 表 t2 中有name字段,直接返回
- 表 t2 中没有 addr 字段,就会找 t2 的元表,看元表中有没有__index 字段,找到后调用对应的值。
Lua 读取表中元素的规则如下:
- 在表中查找,如果找到,返回该元素,找不到则继续
- 判断该表是否有元表,如果没有元表,返回 nil,有元表则继续
- 判断元表有没有 __index 方法,如果 __index 方法为 nil,则返回 nil;如果 index 方法是一个表,则重复 1、2、3;如果 __index 方法是一个函数,Lua会以表和键为参数调用该函数,并返回该函数的返回值
如果我们希望在访问一个表时不调用 __index 元方法,可以使用原生函数 rawget:
rawget(t2, addr) -- 输出 nil • 1
__newindex 元方法
当查询表中一个不存在的字段时,Lua会从该表的元表中的 __index 继续寻找;当对表中一个不存在的字段进行赋值时,就会触发 __newindex 元方法,此时__newindex 的值需要分为两种情况:
(1)__newindex 指向的是一个表:
t3 = {id = 3, name = "lwang", age = 28} meta_t3 = { addr = "beijing" } meta_t3.__newindex = meta_t3 setmetatable(t3, meta_t3) print(t3.addr) -- nil print(meta_t3.addr) -- beijing t3.addr = "shenzhen" print(t3.addr) -- nil t3 不存在addr字段,找元表meta_t3的__newindex 指向meta_t3表 会为meta_t3的addr赋值,而不进行自身赋值 print(meta_t3.addr) -- shenzhen, 值被t3修改
运行结果:
(2)__newindex 指向的是函数:
t3 = {id = 3, name = "lwang", age = 28} meta_t3 = { addr = "beijing" } meta_t3.__newindex = function() print("这里是meta_t3 __newindex 元方法调用") end setmetatable(t3, meta_t3) t3.addr = "shenzhen" -- 输出:这里是meta_t3 __newindex 元方法调用 print(t3.addr) -- nil
运行结果:
__tostring 元方法
用来接管表的返回值,按照我们预定的格式输出,还是以两个表相加为例:
a, b = {1, 2, 3}, {10, 20} meta_ta = { __add = function(t1, t2) for k, v in pairs(t2) do table.insert(t1, v) end return t1 end, -- __tostring 接管本表的返回值,其中 .. 相当于c++中字符串拼接 __tostring = function(t) s = "" for k, v in pairs(t) do s = s..v.." " end return s end } setmetatable(a, meta_ta) -- 如果不实现 __tostring 方法,直接 print(table) 打印出来是table的地址 print(a) -- 1 2 3 c = a + b print(c) -- 1 2 3 10 20
运行结果
__call 和 __gc 元方法
__call 可以让表以类似函数的方式那样调用,比如:tab(arg1, arg2 …)
__gc 有点像c++中析构函数,释放资源
t1 = {10, 20, 30} meta_t1 = { __call = function(t, ...) print(t, ...) end, --元表中用一个以字符串 " __gc " 为索引的域,那么就标记了这个对象需要触发终结器 __gc = function() print("call gc func") end } setmetatable(t1, meta_t1) t1("panda", 10, true) -- table: 0xafc780 panda 10 true print("app close..")
其中,__call 方法的第一个参数为表地址,剩余参数用…表示。