元表

*此内容使用人工智能(Beta)翻译,可能包含错误。若要查看英文页面,请点按 此处

元表使表变得比以前更强大。它们附加到数据上,并包含称为元方法的值。当与附加到其上的数据执行某个操作时,元方法会被触发。

操作元表

添加和查找表的元表的两个主要函数是 setmetatable()getmetatable()

local x = {}
local metaTable = {} -- 元表也是表!
setmetatable(x, metaTable) -- 给 x 一个名为 metaTable 的元表!
print(getmetatable(x)) --> table: [十六进制内存地址]

setmetatable() 函数还会返回你正在设置元表的表,因此这两个脚本做的事情是相同的:

local x = {}
setmetatable(x, {})
local x = setmetatable({}, {})

元方法

元方法是存储在元表中的函数。它们可以用于调用表、添加表,甚至对表进行除法操作。以下是可用的元方法列表:

方法描述
__index(table, index)table[index] 被索引且 table[index]nil 时触发。也可以设置为一个表,在这种情况下,该表将被索引。
__newindex(table, index, value)当尝试设置 table[index] (table[index] = value) 时触发,如果 table[index]nil。也可以设置为一个表,在这种情况下,该表将被索引。
__call(table, ...)当表像函数一样被调用时触发,... 是传递的参数。
__concat(table, value)当在表上使用 .. 连接运算符时触发。
__unm(table)当在表上使用一元 运算符时触发。
__add(table, value)+ 加法运算符。
__sub(table, value) 减法运算符。
__mul(table, value)* 乘法运算符。
__div(table, value)/ 除法运算符。
__idiv(table, value)// 向下取整除法运算符。
__mod(table, value)% 取模运算符。
__pow(table, value)^ 乘方运算符。
__tostring(table)当调用 tostring 时触发。
__metatable如果存在,锁定元表,使 getmetatable() 返回此值而不是元表,并且 setmetatable() 会出错。非函数值。
__eq(table, value)== 等于运算符¹
__lt(table, value)< 小于运算符¹
__le(table, value)<= 运算符¹
__mode用于弱表,声明表的键和/或值是否为弱。请参见 使用弱表
__len(table)当在对象上使用 # 长度运算符时触发。
__iter(table)用于在使用广义迭代时表示自定义迭代器。

需要注意的是,在为算术或关系元方法编写函数时,两个函数参数在触发元方法的表和另一个值之间是可互换的。例如,在进行标量的向量运算时,除法不是交换的。因此,如果你为自己的 vector2 类编写元方法,你需要小心考虑这两种情况。

local vector2 = {__type = "vector2"}
local mt = {__index = vector2}
function mt.__div(a, b)
if type(a) == "number" then
-- a 是标量,b 是向量
local scalar, vector = a, b
return vector2.new(scalar / vector.x, scalar / vector.y)
elseif type(b) == "number" then
-- a 是向量,b 是标量
local vector, scalar = a, b
return vector2.new(vector.x / scalar, vector.y / scalar)
elseif (a.__type and a.__type == "vector2" and b.__type and b.__type == "vector2") then
-- a 和 b 都是向量
return vector2.new(a.x / b.x, a.y / b.y)
end
end
function mt.__tostring(t)
return t.x .. ", " .. t.y
end
function vector2.new(x, y)
local self = setmetatable({}, mt)
self.x = x or 0
self.y = y or 0
return self
end
local a = vector2.new(10, 5)
local b = vector2.new(-3, 4)
print(a / b) -- -3.3333333333333, 1.25
print(b / a) -- -0.3, 0.8
print(2 / a) -- 0.2, 0.4
print(a / 2) -- 5, 2.5

用法

有很多方法可以使用元表。

对表应用一元负号运算符

__unm 元方法处理一元负号运算符。使用它来否定表中的每个值:

local metatable = {
__unm = function(t) -- __unm 是一元 - 运算符
local negated = {}
for key, value in t do
negated[key] = -value -- 否定此表中的所有值
end
return negated -- 返回表
end
}
local table1 = setmetatable({10, 11, 12}, metatable)
print(table.concat(-table1, "; ")) --> -10; -11; -12

从另一个表中索引缺失的值

以下是使用 __index 声明内容的一种有趣方式:

local metatable = {
__index = {x = 1}
}
local t = setmetatable({}, metatable)
print(t.x) --> 1

当在表中索引 x 且未找到时,__index 被触发。Luau 然后在 __index 表中搜索名为 x 的索引,并找到后返回该值。

像函数一样调用表

元表可以做的事情远不止提供缺失的值。以此为例:

local t = {10, 20, 30}
print(t(5))

通常你不能调用一个表,但通过元表你可以:

local metatable = {
__call = function(t, param)
local sum = {}
for i, value in ipairs(t) do
sum[i] = value + param -- 将参数 (5) 加到值上,然后放入新表 (t) 中。
end
return unpack(sum) -- 返回单个表值
end
}
local t = setmetatable({10, 20, 30}, metatable)
print(t(5)) --> 15 25 35

对表应用加法运算符

你还可以做更多的事情,例如添加表:

local table1 = {10, 11, 12}
local table2 = {13, 14, 15}
for k, v in table1 + table2 do
print(k, v)
end

这将报错,表示你试图对一个表进行算术运算,但在尝试使用元表时它可以工作:

local metatable = {
__add = function(t1, t2)
local sum = {}
for key, value in t1 do
sum[key] = value
end
for key, value in t2 do
if sum[key] then
sum[key] += value
else
sum[key] = value
end
end
return sum
end
}
local table1 = setmetatable({10, 11, 12}, metatable)
local table2 = setmetatable({13, 14, 15}, metatable)
for k, v in table1 + table2 do
print(k, v)
end

在不调用元方法的情况下设置值

在使用元表时,你可能会遇到一些问题。如果你需要使用 __index 元方法在表中创建新值,但该表的元表也有 __newindex 元方法,你将希望使用 Luau 内置函数 rawset() 来设置值而不调用任何元方法。以下代码示例展示了如果不使用这些函数会发生什么。

local t = setmetatable({}, {
__index = function(self, i)
self[i] = i * 10
return self[i]
end,
__newindex = function(self, i, v)
-- 不要以正常方式将值设置到表中
end
})
print(t[1]) -- 导致栈溢出

栈溢出发生在你尝试从自身调用函数太多次时。在上面的 __index 函数中,self[i] 被设置为一个值,因此当它到达下一行时,self[i] 应该存在,并且大概不会调用 __index 元方法。问题是 __newindex 不允许你设置该值。它的存在阻止了通过标准的 t[i] = v 方法将值添加到表中。为了绕过这个问题,使用 rawset() 函数:

local t = setmetatable({}, {
__index = function(self, i)
rawset(self, i, i * 10)
return self[i]
end,
__newindex = function(self, i, v)
-- 不要以正常方式将值设置到表中
end
})
print(t[1]) --> 10

使用弱表

持有表、函数、线程和 用户数据 的变量,包括 Instances,存储对这些对象的引用,而不是它们的副本。将一个对象分配给另一个变量会创建对同一对象的另一个引用:

local map = {key = 1}
local mapReference = map
print(map == mapReference) --> true
print(map == {key = 1}) --> false
local function func() end
local funcReference = func
print(func == funcReference) --> true
local part = Instance.new("Part")
local partReference = part
print(part == partReference) --> true

不再通过强引用可达的对象变得可以被垃圾回收。将一个变量设置为 nil 并不会使对象变得可以被垃圾回收,如果另一个强引用仍然指向该对象:

local child = {}
local parent = {Child = child}
child = nil
print(parent.Child) --> table: [十六进制内存地址]

尽管 child 不再引用子表,但 parent.Child 仍然引用它。只要父表保持可达,它的引用就会使子表保持可达。

一个 弱表 持有键、值或两者,而不会阻止垃圾收集器回收其他不可达对象。设置表的元表的 __mode 字段以控制哪些引用是弱的:

  • "k" 使键为弱。
  • "v" 使值为弱。
  • "kv""vk" 使键和值都为弱。

以下代码示例创建一个具有弱值的表:

local children = setmetatable({}, {
__mode = "v",
})
do
local child = {}
children.example = child
end

do 块结束后,存储在 children.example 中的值没有强引用,变得可以被垃圾回收。当垃圾收集器回收子表时,Luau 会从 children 中删除其条目。垃圾收集是非确定性的,因此不要依赖条目消失的时间。

你留下的任何模式都保持强。__mode = "v" 表中的键保持强引用,因此作为键存储的对象保持可达,其条目保持在表中。

对于 Instances,垃圾收集适用于 Luau 引用,而不是对象本身。引擎将实例数据存储在 Luau VM 之外,因此收集 Luau 引用并不会删除实例,而仍然在数据模型中的实例不会保持其 Luau 引用。要释放实例,请调用 Instance:Destroy() 并清除对它的所有剩余引用。

使用集合数据类型

集合 是一个没有顺序且没有重复元素的项目集合。一个项目要么 集合中,要么 不在 集合中。使用元表,你可以在 Luau 脚本中构建和操作集合。

基本方法

以下代码包括基本的集合功能,让你构建新集合、添加和移除项目、检查集合是否包含项目,并输出集合的内容。

local Set = {}
Set.__index = Set
-- 从可选的项目列表构建集合的函数
function Set.new(items)
local newSet = {}
for key, value in items or {} do
newSet[value] = true
end
return setmetatable(newSet, Set)
end
-- 向集合添加项目的函数
function Set:add(item)
self[item] = true
end
-- 从集合中移除项目的函数
function Set:remove(item)
self[item] = nil
end
-- 检查集合是否包含项目的函数
function Set:contains(item)
return self[item] == true
end
-- 将集合输出为逗号分隔的列表以供调试的函数
function Set:output()
local elems = {}
for key, value in self do
table.insert(elems, tostring(key))
end
print(table.concat(elems, ", "))
end

创建集合

可以通过调用 Set.new() 并传入可选的项目数组来构建新集合。

local fruits = Set.new({"Apple", "Lemon", "Orange", "Cherry", "Lime", "Peach"})

请注意,根据定义,集合没有排序的概念。

添加项目

可以通过 Set:add() 方法向现有集合添加项目。

local fruits = Set.new({"Apple", "Lemon", "Orange", "Cherry", "Lime", "Peach"})
fruits:add("Mango")

移除项目

要从集合中移除项目,请调用 Set:remove() 并传入项目名称。

local fruits = Set.new({"Apple", "Lemon", "Orange", "Cherry", "Lime", "Peach"})
fruits:remove("Orange")

检查项目

要检查集合是否包含特定项目,请使用 Set:contains()

local fruits = Set.new({"Apple", "Lemon", "Orange", "Cherry", "Lime", "Peach"})
local result1 = fruits:contains("Cherry")
print(result1) -- true
local result2 = fruits:contains("Watermelon")
print(result2) -- false

其他方法

可以为集合实现其他有用的操作,让你比较集合之间的项目、合并集合或从一个集合中减去另一个集合。

交集

当将集合视为维恩图时,可以如下获取两个集合的 交集,即出现在 两个 集合中的项目。

local function getIntersection(set1, set2)
local result = Set.new()
for key, value in set1 do
if set2:contains(key) then
result:add(key)
end
end
return result
end
local freshFruits = Set.new({"Mango", "Lemon", "Orange", "Cherry", "Lime", "Peach"})
local frozenFruits = Set.new({"Mango", "Peach", "Pineapple"})
local commonFruits = getIntersection(freshFruits, frozenFruits)
commonFruits:output() -- Mango, Peach

并集

你可以使用以下函数获取两个集合的 并集,即两个集合中所有项目的集合,没有重复项。请注意,此函数使用元表的 __add 方法提供 set1 + set2 的加法快捷方式。

function Set:__add(otherSet)
local result = Set.new()
for entry in self do
result[entry] = true
end
for entry in otherSet do
result[entry] = true
end
return result
end
local sweetFruits = Set.new({"Apple", "Mango", "Cherry", "Peach"})
local sourFruits = Set.new({"Lemon", "Lime"})
local allFruits = sweetFruits + sourFruits
allFruits:output() -- Peach, Lime, Apple, Cherry, Lemon, Mango

差集

你可以通过以下函数从一个集合中移除另一个集合中的所有项目。与上面的函数类似,这使用元表的 __sub 方法提供 set1 - set2 的减法快捷方式。

function Set:__sub(otherSet)
local result = Set.new()
for entry in self do
result[entry] = true
end
for entry in otherSet do
result[entry] = nil
end
return result
end
local allFruits = Set.new({"Apple", "Lemon", "Mango", "Cherry", "Lime", "Peach"})
local sourFruits = Set.new({"Lemon", "Lime"})
local sweetFruits = allFruits - sourFruits
sweetFruits:output() -- Mango, Apple, Cherry, Peach
©2026 Roblox Corporation、Roblox、Roblox 标志及 Powering Imagination 是我们在美国及其他国家或地区的注册与未注册商标。