Lua学习笔记:Lua里metatable元表的使用

2024-09-13 07:55:09 浏览数 (2)

元表简介

  • 元表: Lua 中的每个值都可以有一个 元表。 这个 元表 其实就是一个普通的 Lua 表, 它用于定义原始值在特定操作下的行为。 如果你想改变一个值在特定操作下的行为,你可以在它的元表中设置对应域。 例如,当你对非数字值做加操作时, Lua 会检查该值的元表中的 "__add" 域下的函数。
  • 元表主要用于定义表的行为:例如如何处理索引不存在的情况、如何进行相关运算等。元表提供了一些特殊的字段(元方法),比 如 __index、__newindex、__add、__tostring 等。
    • 元表可以让一个基础的自定义数据类型 实现 内建行为(内建函数、运算符等)
    • 元表可以实现一个类
    • 元表可以看作一个普通表的 方法类,类似于C 中的纯虚类
  • 如何设置元表? 可以通过 setmetatable 函数给一个表设置元表,getmetatable 来获取任何值的元表
代码语言:lua复制
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查找元素的规则如下:
    1. 在表中查找,找到则返回,找不到则继续
    2. 判断是否有元表,没有返回nil,有则继续
    3. 判断元表有无__index方法,如果该方法为nil,则返回nil;如果是一个表,则重复1-3;
    4. 如果是一个函数,则返回函数的返回值(table和key会作为参数传递进去)
代码语言:lua复制
-- 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元方法的
代码语言:lua复制
-- 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 参考手册

0 人点赞