Lua 元表、元方法的总结整理

项目组中使用的cocos2dx-lua 框架,经常看到的类也是由cocos2dx-lua 的 function class(classname, …) 实现的,依据这个去看了Lua元表和元方法,但是看的是云里雾里,现在记录下来,以后有深入学习时再回过头来看看

一、元表(metatable)理解

现在我们访问一个表,但是我们不小心访问到了空值,那么就会返回一个nil,当我们访问表中的空值的时候,不想接收一个nil,想接收一个默认值,此时就需要就会用到元表了。先看一个例子:

 local t = {}
 local mt = {
     __index = {
         a = 10,
         b = 20,
         add = function(a,b)
             return a + b
         end
     }
 }
 print("设置元表前:",t.a)
 setmetatable(t,mt)		-- 设置 t的元表为 mt
 print("设置元表后:",t.a)
 print(string.format( "%d + %d = %d",t.a,t.b,t.add(t.a,t.b) ))

在这里插入图片描述

正常来说,表和表之间是不能相加的,会报错,但是有了元表,可以通过定义元表的方式,对于两个table相加的操作进行定义:

local a = {1, 2, 3} --定义两个表
local b = {4, 5, 6}
local mt = {} --定义一个元表
mt.__add = function(a, b) --定义加字段的函数功能
    local len = #a --取a的长度
    local res = {} --定义一个结果表(用作输出)
    for i = 1, len do --遍历所有表a中的元素
        res[i] = a[i] + b[i] --按照下标访问
    end
    return res --返回结果表
end

setmetatable(a, mt)
setmetatable(b, mt)
local c = a + b
for k, v in ipairs(c) do
    print(k, v)
end

在这里插入图片描述

这个例子有点像是C++中的运算符重载。其中__add就是用于识别“+”号的运算符。通过mt.__add定义一个function,其中参数就是两个相加的table。而setmetatable就是把两个table类型的a和b关联元表,使其可以使用元表中的方法进行运算。

比如:在C++或者Java中经常需要将一个坐标封装成一个二维向量的结构体,然后在里面重载一些加减的操作, 因为普通的加减是对于两个数字的,但是当你想让两个二维向量相加的时候,需要将X相加,然后Y相加,然后返回一个二维向量,此时普通的加号就没有办法完成我们的操作了、这个时候我们就要将加号重载一下。然后当调用加号的对象变成二维向量的时候,系统就会使用我们自己重载的加号,完成两个二维向量的加法,然后返回一个二维向量。

Lua 中的每个值都可以有一个 元表。 这个 元表 就是一个普通的 Lua 表, 它用于定义原始值在特定操作下的行为。 如果你想改变一个值在特定操作下的行为,你可以在它的元表中设置对应域。 例如,当你对非数字值做加操作时, Lua 会检查该值的元表中的 “__add” 域下的函数。 如果能找到,Lua 则调用这个函数来完成加这个操作。

1.lua代码中只能设置table的元表,至于其他类型值的元表只能通过C代码设置。 默认情况下,值是没有元表的, 但字符串库在初始化的时候为字符串类型设置了元表…

2.元表决定了一个对象在数学运算、位运算、比较、连接、 取长度、调用、索引时的行为。 元表还可以定义一个函数,当表对象或用户数据对象在垃圾收集时调用它http://cloudwu.github.io/lua53doc/manual.html#2.5

3.多个table可以共享一个通用的元表,但是每个table只能拥有一个元表

local t = {}
print(getmetatable(t)) --> nil

元表的设置和获取:

1.setmetatable (tableA, tableB)

指定表设置元表。 (你不能在 Lua 中改变其它类型值的元表,那些只能在 C 里做。) 如果 tableBnil, 将指定表的元表移除。 如果原来那张元表有 "__metatable" 域,抛出一个错误。这个函数返回 tableA

2.getmetatable (object)

如果 object 不包含元表,返回 nil 。 否则,如果在该对象的元表中有 "__metatable" 域时返回其关联值, 没有时返回该对象的元表。

local mt = {}
local t  = {}
print(setmetatable(t,mt)) -- 给表t设置元表mt,返回值和下行相同
print(t) 
print(mt) 
print(getmetatable(t)) --获取元表 打印元表地址,它们的地址相同

在这里插入图片描述

3.rawget (table, index)

在不触发任何元方法的情况下 获取 table[index] 的值。 table 必须是一张表; index 可以是任何值。

4.rawset(table, index, value)

在不调用元表的情况下,给table[index]赋值为value,其中参数table必须是一个表,而参数index可以是不为nil的任何值。

二、元方法

我们称元表中的键为事件(event),称值为元方法(metamethod)。前述例子中的事件是"add",元方法是执行加法的函数。

在元表中事件的键值是一个双下划线(__)加事件名的字符串; 键关联的那些值被称为 元方法。 在上一个例子中,__add 就是键值, 对应的元方法是执行加操作的函数。

__add(a, b) --加法
__sub(a, b) --减法
__mul(a, b) --乘法
__div(a, b) --除法
__mod(a, b) --取模
__pow(a, b) --乘幂
__unm(a) --相反数
__concat(a, b) --连接
__len(a) --长度
__eq(a, b) --相等
__lt(a, b) --小于
__le(a, b) --小于等于
__index(a, b) --索引查询
__newindex(a, b, c) --索引更新(PS:不懂的话,后面会有讲)
__call(a, ...) --执行方法调用
__tostring(a) --字符串输出
__metatable --保护元表

1. __metatable :

local mt = {}
local t  = {}
print(mt)
print(t)

在Lua中,函数setmetatable和getmetatable函数会用到元表中的一个字段,用于保护元表,该字段是__metatable。当我们想要保护集合的元表,是用户既不能看也不能修改集合的元表,那么就需要使用__metatable字段了;

当设置了该字段时,getmetatable就会返回这个字段的值

local mt = {__metatable = "metatable"}
local t  = {}
setmetatable(t,mt)
print(getmetatable(t))      -- -》metatable
setmetatable(t, {})     -- -》报错

在这里插入图片描述

2. __index :

索引 table[key]。 当 table 不是表或是表 table 中不存在 key 这个键时,这个事件被触发。 此时,会读出 table 相应的元方法。

尽管名字取成这样, 这个事件的元方法其实可以是一个函数也可以是一张表。 如果它是一个函数,则以 tablekey 作为参数调用它。 如果它是一张表,最终的结果就是以 key 取索引这张表的结果。 (这个索引过程是走常规的流程,而不是直接索引, 所以这次索引有可能引发另一次元方法。)

当我们的元表里面有__index关键字的时候。访问规则就有了一些变化。

lua查找表中的元素时规则如下:

1.在表中查找,如果找到,返回该元素,找不到则继续
2.判断该表是否有元表,如果没有元表,返回nil,有元表则继续
3.判断元表有没有__ index方法,如果__ index方法为nil,则返回nil;如果__ index方法是一个表,则重复1、2、3;如果__index方法是一个函数,则返回该函数的返回值

-- 元方法为值
local t1 = setmetatable({key1 = "hello"}, { __index = {key2 = "world" } })
print(t1.key1)
print(t1.key2)
--元方法为函数
local t2 = setmetatable({key1 = "Lua"}, {
  __index = function(t2, key)
    if key == "key2" then
      return "good"
    else
      return nil
    end
  end
})
print(t2.key1)
print(t2.key2)
-- 元方法为表
local Windows = {} -- 创建一个命名空间
Windows.default = {x = 0, y = 0, width = 100, height = 100, color = {r = 255, g = 255, b = 255}}
Windows.mt = {} -- 创建元表
-- 声明构造函数
function Windows.new(o)
     setmetatable(o, Windows.mt)
     return o
end
-- 定义__index元方法
Windows.mt.__index = Windows.default
local win = Windows.new({x = 10, y = 10})
print(win.x)                -- >10 访问自身已经拥有的值
print(win.width)            -- >100 访问default表中的值
print(win.color.r)          -- >255 访问default表中的值

在这里插入图片描述

3.__newindex

__newindex用于更新table中的数据,而__index用于查询table中的数据.

当对一个table中不存在的索引赋值时,在Lua中是按照以下步骤进行的:

Lua解释器先判断这个table是否有元表;

  1. 如果有了元表,就查找元表中是否有__newindex元方法;如果没有元表,就直接添加这个索引,然后对应的赋值;
  2. 如果有这个__newindex元方法,Lua解释器就执行它,而不是执行赋值;
  3. 如果这个__newindex对应的不是一个函数,而是一个table时,Lua解释器就在这个table中执行赋值,而不是对原来的table。
-- 定义一个table
local role = {
    name = "LiSi",
    age = 18,
}
-- 设置元表
local mt = {
    __newindex = function(table, key, value)
        print(key .. "字段是不存在的,不要试图给它赋值!");
        -- rawset(table, key, value);       -- 在不调用元表的情况下,给table[index]赋值为value
    end
}
-- 设置原表
setmetatable(role, mt);
-- 修改和新增值
role.age = 24;
role.sex = 'man';

print(role.name)
print(role.age)
print(role.sex)

在这里插入图片描述

通过给一个table给另一个table的字段赋值

local role = {
    name = "勇士"
}
local monster = {
    name = "巨龙"
}

local t1 = {}
local mt = {
    __index = role,
    __newindex  = monster,
}
setmetatable(t1, mt);

print("monster的名字,赋值前:" .. monster.name);       -- -》monster的名字,赋值前:巨龙
t1.name = "哥布林";
print("monster的名字,赋值后:" .. monster.name);       -- -》monster的名字,赋值前:哥布林
print("role的名字:" .. t1.name);                      -- -》role的名字:勇士

当给t1的name字段赋值后,monster的name字段反而被赋值了,而role的name字段仍然没有发生变化。

这就是__newindex的规则:

a.如果__newindex是一个函数,则在给table不存在的字段赋值时,会调用这个函数。
b.如果__newindex是一个table,则在给table不存在的字段赋值时,会直接给**__newindex的table**赋值。

4.__call

call顾名思义就是调用 ,当调用表的时候,Lua就会使用这个函数,前提是你的元表里面有这个__call的定义。

我们平时访问表。就是通过[ ]的形式来访问,而且一次只能访问一个元素。

但是当我们为元表定义了__call关键字的时候,我们就可以通过括号来访问表了。

local metatable = {
__call = function (table,...)
    local ret = ""
    for i = 1, select('#', ...) do
        local arg = select(i, ...);
        print("arg", arg);
        ret = ret..arg.."="..table[arg]..","
    end
    return ret
end
}
 
local mytable = setmetatable({"one","two","three","four","five"},metatable)
 
local str = mytable(2,3,4)
print(str)

在这里插入图片描述

5.__tostring

函数print总是调用tostring来进行格式化输出,当格式化任意值时,tostring会检查该值是否有一个__tostring的元方法,如果有这个元方法,tostring就用该值作为参数来调用这个元方法,剩下实际的格式化操作就由__tostring元方法引用的函数去完成,该函数最终返回一个格式化完成的字符串。

重新定义__tostring元方法,让print可以格式化打印出table类型的数据。,然后遍历表里面的内容,将其拼接成字符串

local mytable = {"Python","Java","C#","C++","Lua"}
setmetatable(mytable,{
    __tostring = function (table)
        local str = ""
        for k,v in pairs(table) do
            str = str..v.."; "
        end
        return str
    end})
 
print(mytable)

在这里插入图片描述

三、运算符重载

示例:使用关键字:__add来代表我们对表的操作,然后我们自己定义一个函数,用来代替本来的加法操作。

local myMetaTable = {
    __add = function(table,newtable)
        local m_ntable = #table
        for k,v in pairs(newtable) do
            m_ntable = m_ntable +1
            table[m_ntable] = v
        end
        return table
    end
}
 
local myTable = setmetatable({"Java","C++","C#","Go"},myMetaTable)
 
local newtable = {"PHP","Python"}
myTable = myTable + newtable
 
for k,v in pairs(myTable) do
    print(v)
end

在这里插入图片描述

以下运算符都可以实现重载,这里不一一实现了。

模式描述
__add对应的运算符 ‘+’.
__sub对应的运算符 ‘-’.
__mul对应的运算符 ‘*’.
__div对应的运算符 ‘/’.
__mod对应的运算符 ‘%’.
__unm对应的运算符 ‘-’.
__concat对应的运算符 ‘…’.
__eq对应的运算符 ‘==’.
__lt对应的运算符 ‘<’.
__le对应的运算符 ‘<=’.

四、只读的table

local function readOnly(t)
    local newT = {};
    local mt = {
        __index = t,
        __newindex = function()
            error("别想修改我!")
        end
    }
    setmetatable(newT,mt)
    return newT
end

local days = readOnly({"周一","周二","周四"})
print(days[2])
days[2] = "周三被吃了?"

在这里插入图片描述

a.首先,readOnly会创建一个新的table,然后把我们传进去的table作为__index元方法。

b.元表里还增加了__newindex,用来阻止不存在字段的赋值操作。

c.readOnly返回的table已经不是我们原来的table了,它是一个空的table,但是它被设置了一个新的元表。

d.开始对days执行赋值操作:days[2] = “周三被吃了?” 。

e.days是一个空的table,所以它不存在这个字段,也因此,会调用__newindex元方法,赋值失败。

f.如果只是调用days,不进行赋值,如:print(days[2]); 则能正常输出字段值,因为days的元表里有__index元方法。虽然days中不存在2这个字段,但是可以通过__index找到这个字段。

总而言之,最终,days成为了一个只可以读取,不能进行赋值操作的table。

参考链接:https://blog.csdn.net/hducollins/article/details/90514596
https://www.jb51.net/article/55152.htm

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值