Lua 元表(Metatable) 和元方法(Metamethod)

Lua中的元表允许改变table行为,通过元方法定义如加法等操作。每个值可有元表,元表中双下划线开头的字段关联元方法。如`__add`用于定义两个table相加。`getmetatable`获取元表,`setmetatable`设置元表。`__index`元方法用于访问不存在的表元素,可为函数或表。`__newindex`用于更新表元素,未找到索引时调用。此外,元表还可控制如乘法、比较等操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这段时间过了一遍Lua语法,有几个点理解起来不是那么容易,自己也是看了好几遍,所以单独拎出来记录一下。下面步入正题。

在Lua table中我们可以访问对应的key来得到value值,但是却无法对两个table进行操作。因此Lua提供了元表,允许我们改变table的行为,每个行为关联了对应的元方法。Lua中的每个值都可以有一个元表。这个元表就是一个普通的Lua表,它用于原始值在特定操作下的行为。如果你想改变一个值在特定操作下的行为,你可以在它的元表中设置对应域。例如,使用元表我们可以定义Lua如何计算两个table的相加操作a+b。当Lua试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫"__add"的字段,若找到,则调用对应的值。"__add "等即时字段,其对应的值(往往是一个函数或是table)就是"元方法".

在元表中事件的键值时一个双下划线(__)加事件名的字符串;键关联的那些值被称为元方法。在上一个例子中,_add就是键值,对应的元方法是执行加操作的函数。你可以用getmetatable函数来获取任何值的元表、Lua使用直接访问的方式从元表中查询元方法。所以从对象o中获取时间ev的元方法等价于下面的代码

rawget(getmetatable(o) or {},"__ev")

关于rawget,简单介绍一下rawget(table,index)

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

有两个很重要的函数来处理元表:

setmetatable(table,metatable):对指定table设置元表,如果元表中存在_metatable键值,setmetatable会失败。

你可以使用setmetatable来替换一张表的元表。在Lua中,你不可以改变表以外其他类型的值得元表(除非你使用调试库);若想改变这些非表类型的值得元表,请使用C API。

表和完全用户数据有独立的元表(当然,多个表和用户数据可以共享一个元表)。其他类型的值按类型共享元表;也就是说所有的数字都共享一个元表,所有的字符串共享另一个元表等等。默认情况下,值是没有元表的,但字符串库在初始化的时候为字符串类型设置了元表。字符串库中的所有函数躲在表string中,它还将其设置为字符串元表的__index域。因此,你可以以面向对象的形式使用字符串函数。例如,string.byte(s,i)可以写成s:byte(i)。

元表决定了一个对象在数学运算,位运算,比较,连接,取长度,调用,索引时的行为。元表还可以定义一个函数,当表对象或用户数据对象在来及回收时调用它。

对于一元操作符(取负,求长度,位反),元方法调用的时候,第二个参数是个哑元,其值等于第一个参数。这样处理仅仅是为了简化Lua的内部实现(这样处理可以让所有的操作都和二元操作一致),这个行为有可能在将来的版本中移除。(使用这个额外参数的行为都是不确定的。)

getmetatable(table):返回对象的元素(metatable).

以下实例演示了如何对指定的表设置元表:

mytable = {}		--普通表
mymetatable = {}	--元表
setmetatable(mytable,mymetatable) --把mymetatable 设为mytable的元表

以上代码也可以直接写成一行:

mytable = setmetatable({},{})

以下为返回对象元表:

getmetatable(mytable)    --这里返回mymetatable

__index元方法

这是metatable最常用的键

当你通过键来访问table的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)中的__index键。如果__index 包含一个表格,Lua会在表格中查找对应的键。

如果__index包含一个函数的话,Lua就会调用那个函数,table和键会作为参数传递给函数。

__index元方法查看表中元素是否存在,如果不存在,返回结构为nil;如果存在则由__index返回结果。

mytable = setmetatable({key1 = "value1"},
	{__index = function(mytable,key)
	if key == "key2" then 
		return "metatablevalue"
	else 
		return nil
	end 
end 
})
print(mytable.key1,mytable.key2)

实际输出结果为:

value1 metatablevalue

实例解析:
mytable表赋值为{key1 = "value1"}

mytable设置了元表,元方法为_index。

在mytable表中查找key1,如果找到,返回该元素,找不到则继续。

在mytable表中查找key2,如果找到,返回metatablevalue,找不到则继续。

判断元素有没有__index方法,如果__index方法是一个函数,则调用该函数。

元方法中查看是否传入"key2"键的参数(mytable.key2已设置),如果传入"key2"参数返回"metatablevalue",否则返回mytable对应的键值。

我们可以将以上代码简单写成:

mytable  = setmetatable({key1 = "value1"},{__index = {key2 = "metatablevalue"}})
print(mytable.key1,mytable.key2)

总结

Lua 查找一个表元素时的规则,其实就是如下3个步骤

1.在表中查找,如果找打,返回该元素,找不到则继续

2.判断该表是否有元素,如果没有元素,返回nil,有元素则继续。

3.判断元素有没有__index方法,如果__index方法为nil ,则返回nil;如果__index方法是一个表,则重复1、2、3;如果__index方法是一个函数,则返回该函数的返回值。

__newindex元方法
__newindex元方法用来对标配更新,__index则用来对表访问。

当你给表的一个缺少的索引赋值,解释器就会查找__newindex元方法:如果存在则调用这个函数而不是进行赋值操作。以下实例演示了_newindex元方法的应用:

mymetatable = {}
mytable = setmetatable({key1 = "value1"},{__newindex = mymetatable})
print(mytable.key1)
mytable.newkey = "新值2"
print(mytable.newkey,mymetatable.newkey)
mytable.key1 = "新值1"
print(mytable.key1,mymetatable.key1)

以下实例执行输出结构为:

value1
nil    新值2
新值1    nil

以上实例中表设置了元方法__newindex,在对新索引键(newkey)赋值时(mytable.newkey = "新值2”),会调用元方法,而不进行赋值。而对已存在的索引键(key1),则会进行赋值,而不调用元方法__newindex。

为表添加操作符

以下实例演示了两表相加操作:

-- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用
-- 自定义计算表中最大键值函数 table_maxn,即计算表的元素个数
function table_maxn(t)
    local mn = 0
    for k, v in pairs(t) do
        if mn < k then
            mn = k
        end
    end
    return mn
end

-- 两表相加操作
mytable = setmetatable({ 1, 2, 3 }, {
  __add = function(mytable, newtable)
    for i = 1, table_maxn(newtable) do
      table.insert(mytable, table_maxn(mytable)+1,newtable[i])
    end
    return mytable
  end
})

secondtable = {4,5,6}

mytable = mytable + secondtable
        for k,v in ipairs(mytable) do
print(k,v)
end

以下实例执行输出结果为 :

1    1
2    2
3    3
4    4
5    5
6    6

__add键包含在元素中,并进行相加操作,表中对应的操作列表如下:(注意:__是两个下划线)

接下来是元表可以控制的事件的详细列表。 每个操作都用对应的事件名来区分。 每个事件的键名用加有 '__' 前缀的字符串来表示; 例如 "add" 操作的键名为字符串 "__add"。

  • __add+ 操作。 如果任何不是数字的值(包括不能转换为数字的字符串)做加法, Lua 就会尝试调用元方法。 首先、Lua 检查第一个操作数(即使它是合法的), 如果这个操作数没有为 "__add" 事件定义元方法, Lua 就会接着检查第二个操作数。 一旦 Lua 找到了元方法, 它将把两个操作数作为参数传入元方法, 元方法的结果(调整为单个值)作为这个操作的结果。 如果找不到元方法,将抛出一个错误。
  • __sub- 操作。 行为和 "add" 操作类似。
  • __mul* 操作。 行为和 "add" 操作类似。
  • __div/ 操作。 行为和 "add" 操作类似。
  • __mod% 操作。 行为和 "add" 操作类似。
  • __pow^ (次方)操作。 行为和 "add" 操作类似。
  • __unm- (取负)操作。 行为和 "add" 操作类似。
  • __idiv// (向下取整除法)操作。 行为和 "add" 操作类似。
  • __band& (按位与)操作。 行为和 "add" 操作类似, 不同的是 Lua 会在任何一个操作数无法转换为整数时 (参见 §3.4.3)尝试取元方法。
  • __bor| (按位或)操作。 行为和 "band" 操作类似。
  • __bxor~ (按位异或)操作。 行为和 "band" 操作类似。
  • __bnot~ (按位非)操作。 行为和 "band" 操作类似。
  • __shl<< (左移)操作。 行为和 "band" 操作类似。
  • __shr>> (右移)操作。 行为和 "band" 操作类似。
  • __concat.. (连接)操作。 行为和 "add" 操作类似, 不同的是 Lua 在任何操作数即不是一个字符串 也不是数字(数字总能转换为对应的字符串)的情况下尝试元方法。
  • __len# (取长度)操作。 如果对象不是字符串,Lua 会尝试它的元方法。 如果有元方法,则调用它并将对象以参数形式传入, 而返回值(被调整为单个)则作为结果。 如果对象是一张表且没有元方法, Lua 使用表的取长度操作(参见 §3.4.7)。 其它情况,均抛出错误。
  • __eq== (等于)操作。 和 "add" 操作行为类似, 不同的是 Lua 仅在两个值都是表或都是完全用户数据 且它们不是同一个对象时才尝试元方法。 调用的结果总会被转换为布尔量。
  • __lt< (小于)操作。 和 "add" 操作行为类似, 不同的是 Lua 仅在两个值不全为整数也不全为字符串时才尝试元方法。 调用的结果总会被转换为布尔量。
  • __le<= (小于等于)操作。 和其它操作不同, 小于等于操作可能用到两个不同的事件。 首先,像 "lt" 操作的行为那样,Lua 在两个操作数中查找 "__le" 元方法。 如果一个元方法都找不到,就会再次查找 "__lt" 事件, 它会假设 a <= b 等价于 not (b < a)。 而其它比较操作符类似,其结果会被转换为布尔量。
  • __index索引 table[key]。 当 table 不是表或是表 table 中不存在 key 这个键时,这个事件被触发。 此时,会读出 table 相应的元方法。

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

  • __newindex索引赋值 table[key] = value 。 和索引事件类似,它发生在 table 不是表或是表 table 中不存在 key 这个键的时候。 此时,会读出 table 相应的元方法。

    同索引过程那样, 这个事件的元方法即可以是函数,也可以是一张表。 如果是一个函数, 则以 table、 key、以及 value 为参数传入。 如果是一张表, Lua 对这张表做索引赋值操作。 (这个索引过程是走常规的流程,而不是直接索引赋值, 所以这次索引赋值有可能引发另一次元方法。)

    一旦有了 "newindex" 元方法, Lua 就不再做最初的赋值操作。 (如果有必要,在元方法内部可以调用 rawset 来做赋值。)

  • __call函数调用操作 func(args)。 当 Lua 尝试调用一个非函数的值的时候会触发这个事件 (即 func 不是一个函数)。 查找 func 的元方法, 如果找得到,就调用这个元方法, func 作为第一个参数传入,原来调用的参数(args)后依次排在后面。
### Lua 方法的使用指南 #### 1. 的概念 在 Lua 中,metatable)是一种特殊的数据结构,它允许开发者为定义一些额外的行为。通过设置,可以改变的操作方式,比如重载运算符、修改键值访问逻辑等[^1]。 #### 2. 设置方法 可以通过 `setmetatable` 函数为一个设置。例如: ```lua local myTable = {} local metaTable = {} -- 设置myTable的metaTable setmetatable(myTable, metaTable) ``` #### 3. 方法的功能 方法metamethod)是存储在中的字段,它们决定了当某些操作被触发时如何响应。以下是常见的方法及其作用: - **`__index`**: 当尝试访问一个中不存在的键时会被调用。它可以是一个函数或者另一个。 ```lua local son = {name = "Tom"} local father = {house = "Big House"} setmetatable(son, {__index = father}) print(son.house) -- 输出: Big House ``` - **`__newindex`**: 当试图向中写入一个新键值对时被调用。同样支持函数或作为参数。 ```lua local t = {} local mt = { __newindex = function(tbl, key, value) rawset(tbl, key, value * 2) end } setmetatable(t, mt) t.x = 5 print(t.x) -- 输出: 10 ``` - **`__add`, `__sub`, `__mul`, `__div`, `__mod`, `__pow`**: 定义两个对象之间的算术运算行为。 ```lua local a = setmetatable({value=5}, { __add = function(a,b) return {value=a.value+b.value} end }) local b = setmetatable({value=10}, getmetatable(a)) print((a + b).value) -- 输出: 15 ``` - **`__tostring`**: 控制对象转换成字符串的方式。 ```lua local obj = {} setmetatable(obj, { __tostring = function(o) return "Object with custom string representation" end }) print(tostring(obj)) -- 输出: Object with custom string representation ``` - **`__call`**: 让像函数一样可调用。 ```lua local callable = {} setmetatable(callable, { __call = function() print("Called!") end }) callable() -- 输出: Called! ``` #### 4. 实际应用案例 假设我们需要创建一个计数器类,每次增加都会打印当前数值,并且不允许直接修改内部状态,则可以用如下代码实现: ```lua Counter = {} function Counter:new(initialValue) initialValue = initialValue or 0 local instance = {count = initialValue} self.__index = self setmetatable(instance, self) return instance end function Counter:increment() self.count = self.count + 1 print(self.count) end local c = Counter:new(10) c:increment() -- 输出: 11 c:increment() -- 输出: 12 print(c["count"]) -- 正常读取属性 rawset(c, "count", 99) -- 尝试强制更改 (不推荐) print(c.count) -- 输出仍为前次增量后的结果而非99 ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值