元表使表变得比以前更强大。它们附加到数据上,并包含称为元方法的值。当与附加到其上的数据执行某个操作时,元方法会被触发。
操作元表
添加和查找表的元表的两个主要函数是 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