lua 元表
在table中,我们可以对table中的key,value进行操作处理,但无法对两个table进行加减操作,比如:
local tabA = {1,2,3} local tabB = {4,5} local tabC = tabA + tabB -- Error: attempt to perform arithmetic on local 'tabA' (a table value)
因此,Lua提供了元表,允许我们改变table的行为。
假如两个table进行相加操作时,lua会检查两个表是否存在metatable, 并且检查metatable是否有__add域,即元表方法,如果存在,则通过_add执行相加结果。比如:
local meta = {} meta.__add = function(t1,t2) local tmp = {} for _, v in pairs(t1) do table.insert(tmp,v) end for _, v in pairs(t2) do table.insert(tmp,v) end return tmp end local tab1 = {1,2,3} local tab2 = {4,5} -- 设定元表 setmetatable(tab1,meta) setmetatable(tab2,meta) -- 执行相加操作 local tab3 = tab1 + tab2 print(table.concat(tab3,",")) -- 1,2,3,4,5
Lua默认下是不带元表的,我们可以通过getmetatable,setmetatable来获取或者设定元表的组成,比如:
-- 查询元表是否存在:getmetatable local tabA = {} print(getmetatable(tabA)) -- nil -- 设定元表,设置方法: setmetatable local tabB = {} local meta = {} -- 任何表都可以成为元表 setmetatable(tabB, meta) print(getmetatable(tabB) == meta) -- true
一组相关的表,可以共享一个元表,比如:
-- 元表相关 local meta = {} meta.__add = function(t1,t2) local tmp = {} for _, v in pairs(t1) do table.insert(tmp,v) end for _, v in pairs(t2) do table.insert(tmp,v) end return tmp end local tab1 = {1,2,3} local tab2 = {4,5} -- 设定元表,也可以设定任意一个 --[[ 原因:如果第一个参数存在__add域的元表,lua将使用第一个作为元表方法。 同样,如果第二个参数存在__add域的元表,lua将使用第二个作为元表方法 这是lua选择元表方法的原则
因此下面的两个setmetatable保留一个即可 ]] setmetatable(tab1,meta) setmetatable(tab2,meta) -- 执行相加操作 local tab3 = tab1 + tab2 print(table.concat(tab3,",")) -- 1,2,3,4,5
在lua中,元表都有着对应的域名或者元方法(metaMethod)与运算符相对应,如下(注意: __是两个下划线):
字段 | 说明 | 运算符或者含义 |
__add | 相加 | + |
__mul | 相乘 | * |
__sub | 相减 | - |
__div | 相除 | / |
__unm | 相反 | - |
__mod | 取模 | % |
__pow | 乘幂 | ^ |
__eq | 等于 | == |
__It | 小于 | < |
__Ie | 小于等于 | <= |
__concat | 连接 | .. |
不等于 | 会将a ~=b 转换为 not (a == b) | |
大于 | 会将 a > b 转换为 b < a | |
大于等于 | 会将 a >= b 转换为 b <= a | |
__tostring | 转换字符串 | 将对象转换为字符串 |
__metatable | 元表 | 保护对象的metatable不被修改 |
__call | 函数调用 | |
__mode | 弱引用 | 实现弱引用table |
__index | 访问数据 | 读取元表数据 |
__newindex | 写入数据 | 元表写入数据 |
可参考C库中的ltm.c文件,如下:
// 摘录于Itm.c, 网址:http://www.lua.org/source/5.1/ltm.c.html void luaT_init (lua_State *L) { static const char *const luaT_eventname[] = { /* ORDER TM */ "__index", "__newindex", "__gc", "__mode", "__len", "__eq", "__add", "__sub", "__mul", "__mod", "__pow", "__div", "__idiv", "__band", "__bor", "__bxor", "__shl", "__shr", "__unm", "__bnot", "__lt", "__le", "__concat", "__call" }; int i; for (i=0; i<TM_N; i++) { G(L)->tmname[i] = luaS_new(L, luaT_eventname[i]); luaC_fix(L, obj2gco(G(L)->tmname[i])); /* never collect these names */ } }
我们简单的说明下:
__metatable: 用于保护对象的元表metatable不被修改,比如:
local mt = {} mt.__metatable = "do not modify metatable" local tab = {1,2,3} setmetatable(tab, mt) -- 再次修改下 --setmetatable(tab, {}) -- 报错:cannot change a protected metatable print('--->',getmetatable(tab)) -- "do not modify metatable"
__index:
当访问普通表中不存在的字段时,会返回nil。若元表存在时,会根据__index 继续查找的字段或者元方法。
简单的说,就是读取元表数据。比如:
local prototype = {x = 1, y = 2, width = 10, height = 20} local w = {x = 30, y = 40} -- 设置 __index 用于读取元表数据 setmetatable(w, {__index = prototype}) -- 自己的表中不存在width,height的属性,但是根据__index依然可以读取 print(w.x, w.y, w.width, w.height) -- 30 40 10 20 -- 拓展: 假设我们不想读取元表数据,可使用:rawget(table, index)。比如: print(rawget(w, "x")) -- 30 print(rawget(w, "width")) -- nil
__newindex:
若元表存在时,当为table添加新的数据时,会去调用元表中的__newindex写入数据。
简单的说,就是为元表写入数据。比如:
local metaTab = {} -- 元表 local tab = {key = "key"} -- 本表 setmetatable(tab,{__newindex = metaTab}) -- 本表中存在指定的元素 print(tab.key, metaTab.key) -- key nil -- 添加数据,该数据会添加到元表,而不是本表中 tab.newkey = "newkey" print(tab.newkey, metaTab.newkey) -- nil newkey -- 本表中存在该属性,重新赋值 tab.key = "key1" print(tab.key, metaTab.key) -- key1 nil -- 本表中不存在该属性,赋值给元表 tab.newkey = "newkey1" print(tab.newkey, metaTab.newkey) -- nil newkey1 -- 拓展: 添加数据,不想添加到元表中,可使用:rawset(table, key, value) rawset(tab, "addKey", "addKey") print(tab.addKey, metaTab.addKey) -- addKey nil
__mode:
首先在cocos lua中,一般为防止表原有属性的改变,我们会使用 clone,比如:
local tabA = {1,2,3} -- 使用clone拷贝一个新的table,避免修改原有表数据 local tabB = clone(tabA) table.insert(tabB, 4) print('tabA:', table.concat(tabA, ",")) -- 1,2,3 print('tabB:', table.concat(tabB, ",")) -- 1,2,3,4
原因在于:lua表的赋值从本质上来说就是引用。比如:
local tabA = {1,2,3} -- 将tabA 赋值给tabB local tabB = tabA -- tabB 中插入新数据 table.insert(tabB, 4) -- tabA的数据也会发生改变 print('tabA:', table.concat(tabA, ",")) -- 1,2,3,4 print('tabB:', table.concat(tabB, ",")) -- 1,2,3,4
我们可以理解这是一种强引用关系。假设tabA 的数据被tabB引用,然后tabA置为nil。如下:
local tabA = {name = "123"} local tabB = {} tabB[1] = tabA -- tabA置为nil tabA = nil -- 执行垃圾回收 collectgarbage() print(tabB[1].name) -- 123 tabA置为nil后,tabB依然可以访问 for i, v in pairs(tabB) do print(i,v) -- 1 table: 0x09fd3660 使用Lua的垃圾回收,引用数据依然存在 end
类似这种操作,就会很容易导致table因管理不当而出现的内存泄漏问题。
为此lua元表提供了“__mode”,用于保证对象可以被回收,我们可以称之为弱引用。mode引用有三种方式:
1. key值弱引用,使用方法: setmetatable(tab, {__mode = "k"})
2. value值弱引用,使用方法: setmetatable(tab, {__mode = "v"})
3. key和value弱引用,使用方法: setmetatable(tab, {__mode = "kv"})
如果当key或value被设定后,整个key和value都会被移除,比如:
local tabA = {name = "123"} local tabB = {} setmetatable(tabB, {__mode = "v"}) tabB[1] = tabA -- 释放tabA tabA = nil -- 调用垃圾回收 collectgarbage() print("tabB:",tabB[1] or "nil") -- nil 引用已释放
参考:
http://www.runoob.com/lua/lua-metatables.html
https://www.cnblogs.com/blueberryzzz/p/8947446.html
https://blog.csdn.net/u013052238/article/details/105313919