Lua元表(metatable)和元方法(metamethod)

本文主要介绍了Lua中通过元表修改值的行为,以实现table类型变量的非预定义操作,如计算table相加。还介绍了处理元表的函数,以及算数类、关系类元方法的使用。同时探讨了元表保护、__index和__newindex元方法,以及rawget和rawset函数的作用。

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

通常,Lua中的每个值都有一套预定义的操作集合,比如数字是可以相加的,字符串是可以连接的,但是对于两个table类型,则不能直接进行“+”操作。这需要我们进行一些操作。在Lua中有一个元表,也就是上面说的metatable,我们可以通过元表来修改一个值的行为,使其在面对一个非预定义的操作时执行一个指定的操作。比如,现在有两个table类型的变量a和b,我们可以通过metatable定义如何计算表达式a+b,具体的在Lua中是按照以下步骤进行的:

  • 先判断a和b两者之一是否有元表;
  • 检查该元表中是否有一个叫__add的字段;
  • 如果找到了该字段,就调用该字段对应的值,这个值对应的是一个metamethod;
  • 调用__add对应的metamethod计算a和b的值。

上述四个步骤就是计算table类型变量a+b的过程。在Lua中,每个值都有一个元表,table和userdata类型的每个变量都可以有各自独立的元表,而其他类型的值则共享其类型所属的单一元表。

有两个很重要的函数来处理元表:
setmetatable(table,metatable): 对指定 table 设置元表(metatable),如果元表(metatable)中存在 __metatable 键值,setmetatable 会失败。
getmetatable(table): 返回对象的元表(metatable)。

Lua在创建新的table时不会创建元表,例如:

t = {1, 2}
print(getmetatable(t))
> nil

创建新的table时,使用getmetatable去获得元表,返回nil。

我们可以使用setmetatable去设置一个table或userdata类型变量的元表,例如:

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

mt = {}
setmetatable(t, mt)
print(getmetatable(t))
> table: 00000000004aa2e0

任何table都可以作为任何值的元表,而一组相关的table可以共享一个元表,此元表标书了它们共同的行为。一个table甚至可以作为它自己的元表,用于描述其特有的行为。

在Lua中,只能设置table的元表。若要设置其他类型的值的元表,则必须通过C代码来完成。
还存在一个特例,对于字符串,标准的字符串程序库为所有的字符串都设置了一个元表,而其他类型在默认情况下都没有元表。例如:

print(getmetatable("hello world!"))
> table: 00000000004a9ea0

print(getmetatable(10))
> nil

在table中,我们可以重新定义的元方法有一下几个:

模式描述
__add加,对应的运算符“+”
__sub减,对应的运算符“-”
__mul乘,对应的运算符“*”
__div除,对应的运算符“/”
__mod取余,对应的运算符“%”
__pow幂,对应的运算符“^”
__unm相反数,对应的运算符“-”
__concat连接,对应的运算符“. .”
__len长度,对应的运算符“#”
__eq等于,对应的运算符“==”
__lt小于,对应的运算符“<”
__le小于等于,对应的运算符“<=”
__index查询索引
__newindex索引更新
__call执行方法调用
__tostring字符串输出
__metatable保护元表

接下来介绍如何去重新定义这些方法。

算数类的元方法
使用完整的示例代码来详细的说明算数类元方法的使用,所有的方法都放入Set这个table中。如下:

Set = {}
local mt = {}					--会被设为其他table的元表

--根据参数table中的值创建一个新的table,同时将mt设为新table的元表
function Set.new(l)
	local set = {}
	setmetatable(set, mt)		--将mt设为set的元表
	for _, v in pairs(l) do
		set[v] = true			--以表l的元素值作为表set的元素索引
	end
	return set
end

--并集操作
function Set.union(a, b)
	local retSet = Set.new{}	--此处相当于Set.new({})
	for k in pairs(a) do
		retSet[k] = true		--以表a的元素索引名字为索引名字在表retSet中创建元素
	end
	for k in pairs(b) do
		retSet[k] = true		--以表a的元素索引名字为索引名字在表retSet中继续创建元素
	end
	return retSet
end

--交集操作
function Set.intersection(a, b)
	local retSet = Set.new{}
	for k in pairs(a) do
		retSet[k] = b[k]		--以a中元素索引名字去b中寻找相同索引名字的元素内容在retSet中创建元素
	end
	return retSet
end

--打印集合操作
function Set.toString(set)
	local tb = {}
	for k in pairs(set) do
		tb[#tb + 1] = k			--以表set的元素索引名字为内容在表tb中创建元素
	end
	
	return "{" .. table.concat(tb, ",") .. "}"
end

function Set.print(s)
	print(Set.toString(s))
end

现在,定义‘+’来计算两个table的并集,那么就需要让所有用于表示集合的table共享一个元表,并且在该元表中定义如何执行一个加法操作。

--创建一个表相加的元方法:并集
mt.__add = Set.union

加下来,我们可以使用‘+’来求两个table的并集,它会自动调用Set.union函数,并将两个操作数作为参数传入。例如:

set1 = Set.new{10, 20, 30}
set2 = Set.new{1, 2, 3}
set3 = set1 + set2

Set.print(set3)
> {1,2,20,10,30,3}

for k, v in pairs(set3) do
	print(k, v)
end
> 1		true
> 2		true
> 20	true
> 10	true
> 30	true
> 3		true

在上面列举的那些可以重定义的元方法都可以使用上面的方法进行重定义。
现在有一个问题需要讨论一下:set1和set2都有元表,那我们要用谁的元表呢?

  1. 对于二元操作符,如果第一个操作数有元表,并且元表中有需要的字段定义,比如我们这里的__add元方法定义,那么Lua就以这个字段为元方法,而与第二个操作数无关;
  2. 对于二院操作符,如果第一个操作数有元表,但是元表中没有需要的字段定义,那么Lua就去查找第二个操作数的元表;
  3. 如果两个操作数都没有元表,或者没有对应的元方法定义,Lua就会引发一个错误。

关系类的元方法
关系类的元方法有:mt.__le(小于等于)、mt.__lt(小于)、mt.__eq(等于),其他3个关系操作符没有单独的元方法,Lua会将a ~= b转化为not (a == b),将a > b转化为b < a,将a >= b转化为b <= a 。
使用示例代码来详细的说明关系类元方法的使用:

--小于等于
mt.__le = 	function(a, b)
				for k in pairs(a) do
					if not b[k] then	--在a中存在的元素索引名字在b中是否全部存在
						return false
					end
				end
				return true
			end

--小于
mt.__lt = 	function(a, b)
				return a <= b and not (b <= a)	--执行"<="时会调用mt.__le元方法
			end

--等于
mt.__eq = 	function(a, b)
				return a <= b and b <= a
			end

s1 = Set.new{2, 4}
s2 = Set.new{4, 10, 2}
print(s1 <= s2)
print(s1 < s2)
print(s1 >= s2)				--执行s1 >= s2时,Lua会自动转成s2 <= s1
print(s1 >= s1)
print(s1 > s1)
> true
> true
> false
> true
> false

--定义元方法乘,表示集合的交集
mt.__mul = Set.intersection

print(s1 == s2 * s1)
> true

我们会发现,使用getmetatable就可以很轻易的得到元表,使用setmetatable就可以很容易的修改元表,那这样做的风险是不是太大了,那么如何保护我们的元表不被篡改呢?
在Lua中,函数setmetatable和getmetatable函数会用到元表中的一个字段,用于保护元表,该字段是__metatable。当我们想要保护集合的元表,是用户既不能看也不能修改集合的元表,那么就需要使用__metatable字段了;当设置了该字段时,getmetatable就会返回这个字段的值,而setmetatable则会引发一个错误。例如:

set1 = Set.new{}
print(getmetatable(set1))		--此时可以返回元表
> table: 00000000004fa370

mt.__metatable = "you cannot get the metatable"
print(getmetatable(set1))		--此时返回元表时会显示mt.__metatable的值
> you cannot get the metatable

mt1 = {}
setmetatable(set1, mt1)			--此时设置set1会导致错误
> lua test.lua:388: cannot change a protected metatable

__index元方法
是否还记得当我们访问一个table中不存在的字段时,会返回什么值?默认情况下,当我们访问一个table中不存在的字段时,得到的结果是nil。
但是这种状况很容易被改变;Lua是按照以下的步骤决定是返回nil还是其它值的:

  1. 当访问一个table的字段时,如果table有这个字段,则直接返回对应的值;
  2. 当table没有这个字段,则会促使解释器去查找一个叫__index的元方法,接下来就就会调用对应的元方法,返回元方法返回的值;
  3. 如果没有这个元方法,那么就返回nil结果。

下面通过一个实际的例子来说明__index的使用。假设要创建一些描述窗口,每个table中都必须描述一些窗口参数,例如颜色,位置和大小等,这些参数都是有默认值的,因此,我们在创建窗口对象时可以指定那些不同于默认值得参数。

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 = function (table, key)
     return Windows.default[key]
end
 
win = Windows.new({x = 10, y = 10})

print(win.x)				-- >10 访问自身已经拥有的值
print(win.width)			-- >100 访问default表中的值
print(win.color.r)			-- >255 访问default表中的值

> 10
> 100
> 255

根据上面代码的输出,结合上面说的那三步,我们再来看看,print(win.x)时,由于win变量本身就拥有x字段,所以就直接打印了其自身拥有的字段的值;print(win.width),由于win变量本身没有width字段,那么就去查找是否拥有元表,元表中是否有__index对应的元方法,由于存在__index元方法,返回了default表中的width字段的值,print(win.color.r)也是同样的道理。

在实际编程中,__index元方法不必一定是一个函数,它还可以是一个table。当它是一个函数时,Lua以table和不存在key作为参数来调用该函数,这就和上面的代码一样;当它是一个table时,Lua就以相同的方式来重新访问这个table,所以上面的代码也可以是这样的:

Windows.mt.__index = Windows.default

__newindex元方法
__newindex元方法与__index类似,__newindex用于更新table中的数据,而__index用于查询table中的数据。当对一个table中不存在的索引赋值时,在Lua中是按照以下步骤进行的:

  1. Lua解释器先判断这个table是否有元表;
  2. 如果有了元表,就查找元表中是否有__newindex元方法;如果没有元表,就直接添加这个索引,然后对应的赋值;
  3. 如果有这个__newindex元方法,Lua解释器就执行它,而不是执行赋值;
  4. 如果这个__newindex对应的不是一个函数,而是一个table时,Lua解释器就在这个table中执行赋值,而不是对原来的table。

有的时候,我们就不想从__index对应的元方法中查询值,我们也不想更新table时,也不想执行__newindex对应的方法,或者__newindex对应的table。那怎么办?在Lua中,当我们查询table中的值,或者更新table中的值时,不想理那该死的元表,我们可以使用rawget函数,调用rawget(tb, i)就是对table tb进行了一次“原始的(raw)”访问,也就是一次不考虑元表的简单访问;你可能会想,一次原始的访问,没有访问__index对应的元方法,可能有性能的提升,其实一次原始访问并不会加速代码执行的速度。对于__newindex元方法,可以调用rawset(t, k, v)函数,它可以不涉及任何元方法而直接设置table t中与key k相关联的value v。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值