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

 

posted @ 2018-12-07 19:09  Code~  阅读(416)  评论(0编辑  收藏  举报