Lua 编程/表
表是 Lua 中唯一的数据结构,但它们比许多其他语言中的数据结构更灵活。它们也称为字典(因为它们使值对应于索引,就像字典中的定义对应于术语一样)、关联数组(因为它们将值与索引关联,从而使它们成为与索引关联的值数组)、哈希表、符号表、哈希映射和映射。它们是使用表构造函数创建的,表构造函数由两个花括号定义,这些花括号可以选择性地包含以逗号或分号分隔的值。以下示例演示了一个数组(一个有序的值列表)并演示了如何获取数组的长度。
local array = {5, "text", {"more text", 463, "even more text"}, "more text"}
print(#array) --> 4 ; the # operator for strings can also be used for arrays, in which case it will return the number of values in the array
-- This example demonstrates how tables can be nested in other tables. Since tables themselves are values, tables can include other tables. Arrays of tables are called multi-dimensional arrays.
表可以包含除 nil 之外的任何类型的值。这是合乎逻辑的:nil 代表值的缺失。在表中插入“值的缺失”将毫无意义。表中的值可以用逗号或分号分隔,并且可以跨越多行。通常使用逗号作为单行表构造函数,使用分号作为多行表,但这并非必需。
local array = {
"text";
{
"more text, in a nested table";
1432
};
true -- booleans can also be in tables
}
表由字段组成,字段是值的对,其中一个是索引(也称为键),另一个是与该索引对应的值。在数组中,索引始终是数值。在字典中,索引可以是任何值。
local dictionary = {
"text";
text = "more text";
654;
[8] = {}; -- an empty table
func = function() print("tables can even contain functions!") end;
["1213"] = 1213
}
如上面的示例所示,可以像在数组中一样向字典添加值,只需添加值(在这种情况下,索引将是一个数字),用标识符和等于号作为前缀添加值(在这种情况下,索引将是与标识符对应的字符串),或者用括号括起来的值和一个等于号作为前缀添加值(在这种情况下,索引是括号内的值)。后一种方法是使索引对应于值的最佳方式,因为它可以与任何值或表达式一起使用。
可以通过将值对应的索引放在方括号内,在求值为表的表达式之后,来访问表中的值
local dictionary = {number = 6}
print(dictionary["number"]) --> 6
如果索引是一个字符串,并且遵循 Lua 标识符的标准(它不包含空格,不以数字开头,不包含数字、字母和下划线以外的任何字符),则可以通过在字符串前添加点来访问它,而无需使用方括号
local dictionary = {["number"] = 6}
print(dictionary.number) --> 6
前两个示例创建了一个相同的表并打印相同的值,但使用不同的符号定义和访问索引。在包含其他表的表的情况下,也可以通过首先索引第一个表以获取嵌套表,然后索引嵌套表中的值来访问嵌套表中的值
local nested_table = {
[6] = {
func = function() print("6") end
}
}
nested_table[6].func() -- This will access the nested table, access the function that is inside it and then call that function, which will print the number 6.
关于语句的章节描述了两种类型的循环:条件控制循环和计数控制循环。在 Lua 中,还有第三种类型的循环,即 foreach 循环,也称为泛型 for 循环。foreach 循环是一个允许程序员为表中的每个字段执行代码的循环。以下示例演示了一个 foreach 循环,它遍历数字数组中的项,并打印所有索引以及与数字 1 对应的值的总和
local array = {5, 2, 6, 3, 6}
for index, value in next, array do
print(index .. ": " .. value + 1)
end
-- Output:
-- 1: 6
-- 2: 3
-- 3: 7
-- 4: 4
-- 5: 7
以下示例中的两个循环将与先前示例中的循环执行相同的操作。
local array = {5, 2, 6, 3, 6}
for index, value in pairs(array) do
print(index .. ": " .. value + 1)
end
for index, value in ipairs(array) do
print(index .. ": " .. value + 1)
end
第一个示例中显示的方法与先前示例中的第一个循环执行相同的操作。但是,最后一个(使用 ipairs
函数的循环)只会迭代到表中第一个缺失的整数键,这意味着它只会迭代通过按顺序排列的数字值,就像它们在数组中一样。过去有两个函数,名为 table.foreach
和 table.foreachi
,但它们在 Lua 5.1 中被弃用,在 Lua 5.2 中被删除。弃用是应用于功能或实践的一种状态,用于指示该功能或实践已被删除或取代,应避免使用。因此,使用 foreach 循环遍历表比使用这两个函数更可取。
在 Lua 中,元组(一个简单的值列表)和表(将索引映射到值的结构)之间有所区别。对返回多个值的函数的函数调用将求值为一个元组。赋值语句中的一系列值,其中多个变量同时被赋值,也是一个元组。变参表达式(用于变参函数)也是一个元组。因为元组是值的列表,而不是单个值,所以它不能存储在变量中,尽管它可以存储在多个变量中。可以通过将求值为元组的表达式放在表构造函数中,将元组转换为数组。
function return_tuple()
return 5, 6 -- Because this function returns two values, a function call to it evaluates to a tuple.
end
local array = {return_tuple()} -- same as local array = {5, 6}
local a, b = return_tuple() -- same as local a, b = 5, 6
print(return_tuple()) -- same as print(5, 6)
可以使用表库的 unpack
函数将数组解包(将其从表更改为元组),并以数组作为参数
local array = {7, 4, 2}
local a, b, c = table.unpack(array)
print(a, b, c) --> 7, 4, 2
在 Lua 5.2 之前的 Lua 版本中,unpack
函数是基本库的一部分。此后,它已移至表库。
因为表可以包含函数并将名称与这些函数关联,所以它们通常用于创建库。Lua 还具有语法糖,可用于创建方法,即用于操作对象(通常由表表示)的函数。对于不了解面向对象编程的人来说,这可能有点难以理解,这超出了本书的范围,因此不理解这一点并无大碍。以下两个示例执行完全相同的操作
local object = {}
function object.func(self, arg1, arg2)
print(arg1, arg2)
end
object.func(object, 1, 2) --> 1, 2
local object = {}
function object:func(arg1, arg2)
print(arg1, arg2)
end
object:func(1, 2) --> 1, 2
当调用表中的函数时,该函数对应于一个字符串索引,使用冒号而不是点将添加一个隐藏的参数,即表本身。类似地,使用冒号而不是点在表中定义函数将在参数列表中添加一个隐藏的 self
参数。使用冒号定义函数并不意味着必须使用冒号来调用函数,反之亦然,因为它们是完全可互换的。
在 Lua 中对表进行排序相对容易。在大多数情况下,可以使用表库的 sort
函数对表进行排序,这使得一切都变得相对容易。sort
函数按给定顺序对数组中的元素进行就地排序(即不创建新数组)。如果提供函数作为第二个参数,它应该接收数组中的两个元素,并在第一个元素在最终顺序中应该出现在第二个元素之前时返回 true。如果没有提供第二个参数,Lua 将根据 <
运算符对数组中的元素进行排序,该运算符在用于两个数字时返回 true,并且第一个数字小于第二个数字,但它也适用于字符串,在这种情况下它在第一个字符串在词典顺序上小于第二个字符串时返回 true。
元表是表,可用于控制其他表的行为。这通过元方法来实现,即该表中的字段,这些字段指示 Lua 虚拟机在代码尝试以特定方式操作表时应该执行的操作。元方法由其名称定义。例如,__index
元方法告诉 Lua 在代码尝试在表中索引尚未存在的字段时应该执行的操作。可以使用 setmetatable
函数设置表的元表,该函数接受两个表作为参数,第一个表是要设置元表的表,第二个表是要将表元表设置到的元表。还有一个 getmetatable
函数,它返回表的元表。以下代码演示了如何使用元表使表中所有不存在的字段看起来像它们包含一个数字;这些数字是使用 math.random
函数随机生成的
local associative_array = {
defined_field = "this field is defined"
}
setmetatable(associative_array, {
__index = function(self, index)
return math.random(10)
end
})
在上面的例子中,你可以注意到很多东西。其中一个需要注意的是,包含 __index
元方法的字段名称前面有两个下划线。这始终如此:当 Lua 在表的元表中查找元方法时,它会查找与元方法名称相对应且以两个下划线开头的索引。另一个需要注意的是,__index
元方法实际上是一个函数(大多数元方法都是函数,但并非所有都是),它接受两个参数。第一个参数 self
是调用 __index
元方法的表。在这里,我们可以直接引用 associative_array
变量,但这对于单个元表用于多个表时很有用。第二个参数是尝试索引的索引。最后,可以注意到该函数通过返回值来告诉 Lua 虚拟机应该向索引表的代码提供什么。大多数元方法只能是函数,但 __index
元方法也可以是表。如果它是一个表,那么当程序尝试索引表中不存在的字段时,Lua 会在该表中查找相同的索引。如果找到,它将返回 __index
元方法中指定的表中与该索引对应的值。
- __newindex(self, index, value)
__newindex
元方法可用于告诉 Lua 虚拟机,当程序尝试在表中添加新字段时该怎么做。它只能是函数,并且只有在程序尝试写入的索引不存在已有字段的情况下才会调用它。它有三个参数:前两个与__index
元方法的参数相同,第三个是程序尝试设置字段值的 value。- __concat(self, value)
__concat
元方法可用于告诉 Lua 虚拟机,当程序尝试使用连接运算符 (..
) 将值连接到表时该怎么做。- __call(self, ...)
__call
元方法可用于指定当程序尝试调用表时应发生什么。这使得表可以像函数一样工作。在调用表时传递的参数将在self
参数(始终是表本身)之后给出。此元方法可用于返回值,在这种情况下,调用表将返回这些值。- __unm(self)
- 此元方法可用于指定对表使用一元减运算符的效果。
- __tostring(self)
- 此元方法可以是函数,当使用表作为参数调用
tostring
函数时,它会返回tostring
函数应返回的值。 - __add(self, value)
- __sub(self, value)
- __mul(self, value)
- __div(self, value)
- __mod(self, value)
- __pow(self, value)
- 这些元方法可用于分别告诉虚拟机在将值添加到、从表中减去、乘以或除以表时该怎么做。最后两个类似,但分别用于模运算符 (
%
) 和指数运算符 (^
)。 - __eq(self, value)
- 此元方法由相等运算符 (
==
) 使用。只有当比较的两个值具有相同的元表时,它才会使用。不相等运算符 (~=
) 使用此函数结果的反面。 - __lt(self, value)
- __le(self, value)
- 这些元方法由“小于”和“小于或等于”运算符使用。“大于”和“大于或等于”运算符将返回这些元方法返回值的反面。只有当值具有相同的元表时,它们才会使用。
- __gc(self)
- 此元方法由 Lua 在垃圾收集器收集具有此元方法的元表关联的值之前调用。它仅适用于用户数据值,不能用于表。
- __len(self)
- 当对用户数据值使用长度运算符 (
#
) 时,Lua 会调用此元方法。它不能用于表。 - __metatable
- 如果此元方法存在(它可以是任何东西),则对表使用
getmetatable
将返回此元方法的值,而不是元表,并且将不允许使用setmetatable
函数更改表的元表。 - __mode
- 此元方法应为一个字符串,其中可以包含字母“k”或“v”(或两者)。如果存在字母“k”,则表的键将是弱键。如果存在字母“v”,则表的 value 将是弱值。这意味着什么将在稍后与垃圾收集一起解释。
元表虽然普通 Lua 程序只能在表中使用它们,但它们实际上是 Lua 处理运算符和操作的核心机制,理论上它们实际上可以与任何值一起使用。但是,Lua 仅允许它们与使用未公开的 newproxy
函数创建的表和用户数据值一起使用。使用 Lua 的 C API 或调试库,可以设置其他类型的值(如数字和字符串)的元表。
有时希望在不调用元方法的情况下对表执行操作。对于索引、在表中添加字段、检查相等性和获取表的长度,可以使用 rawget
、rawset
、rawequal
和 rawlen
函数。第一个返回在作为第一个参数传递给它的表中,对应于作为第二个参数传递给它的索引的值。第二个将作为第三个参数传递给它的值设置为作为第一个参数传递给它的表中对应于作为第二个参数传递给它的索引的值。第三个返回两个传递给它的值是否相等。最后,第四个返回传递给它的对象的长度(一个整数),该对象必须是表或字符串。
迭代器是与 foreach 循环一起使用的函数。在大多数情况下,迭代器用于循环或遍历数据结构。例如,由 pairs
和 ipairs
函数返回的迭代器分别用于遍历表或数组的元素。例如,pairs
函数返回 next
函数,以及作为参数传递给它的表,这解释了 in pairs(dictionary)
与 in next, dictionary
的等价性,因为前者实际上评估为后者。
迭代器不需要始终与数据结构一起使用,因为迭代器可以针对需要循环的任何情况进行设计。例如,file:lines
函数返回一个迭代器,该迭代器在每次迭代时从文件中返回一行。类似地,string.gmatch
函数返回一个迭代器,该迭代器在每次迭代时返回字符串中模式的匹配项。例如,此代码将打印名为“file.txt”的文件中的所有行。
for line in io.open("file.txt"):lines() do
print(line)
end
迭代器包含三个部分
- 一个转换函数
- 一个状态值
- 一个或多个循环变量
转换函数用于修改循环变量(出现在 for
和 in
之间的变量)的每次循环的值。此函数在每次迭代之前调用,并将上次迭代期间循环变量设置为的值作为参数。该函数应返回一个元组(一个或多个值),其中包含这些变量的新值。循环变量将设置为返回元组的组件,并且循环将进行一次迭代。一旦该迭代完成(只要它没有被 break
或 return
语句中断),转换函数将再次被调用,并返回另一组值,循环变量将被设置为下一个迭代,依此类推。调用转换函数和迭代循环语句的循环将持续到转换函数返回 nil
为止。
除了循环变量之外,转换函数还会传入一个状态值,该状态值在整个循环过程中将保持不变。例如,状态值可用于维护对转换函数正在迭代的数据结构、文件句柄或资源的引用。
下面给出了一个转换函数的示例,该函数将生成一系列不超过 10 的数字。此转换函数只需要一个循环变量,它的值将存储在 value
中。
function seq(state, value)
if (value >= 10) then
return nil -- Returning nil will terminate the loop
else
local new_value = value + 1 -- The next value to use within the loop is the current value of `value` plus 1.
-- This value will be used as the value of `value` the next time this function is called.
return new_value
end
end
通用 for 循环需要一个元组,其中包含转换函数、状态值和循环变量的初始值。此元组可以直接包含在 in
关键字之后。
-- This will display the numbers from 1 to 10.
for value in seq, nil, 0 do
print(value)
end
但是,在大多数情况下,此元组将由函数返回。这允许使用迭代器工厂,这些工厂在被调用时会返回一个新的迭代器,该迭代器可以与通用 for 循环一起使用
function count_to_ten()
local function seq(state, value)
if (value >= 10) then
return nil
else
local new_value = value + 1
return new_value
end
end
return seq, nil, 0
end
for value in count_to_ten() do
print(value)
end
由于 Lua 支持闭包和函数作为一等对象,因此迭代器工厂也可以接受参数,这些参数可以在转换函数中使用。
function count_to(limit)
local function seq(state, value)
if (value >= limit) then
return nil
else
local new_value = value + 1
return new_value
end
end
return seq, nil, 0
end
for value in count_to(10) do -- Print the numbers from 1 to 10
print(value)
end
for value in count_to(20) do -- Print the numbers from 1 to 20
print(value)
end