掌握 Lua 数组 (Table) 的核心操作
精通 Lua 之基石:全面掌握 Lua 数组 (Table) 的核心操作
在 Lua 这门以简洁、高效和可扩展性著称的脚本语言中,table
类型扮演着至关重要的角色。它并非仅仅是一个简单的数据结构,而是 Lua 中构建几乎所有复杂数据类型的基石,包括我们通常意义上理解的数组(或称为序列)、字典(哈希映射)、集合,甚至是面向对象编程中的对象和模块(命名空间)。本文将深入探讨如何将 Lua 的 table
用作数组(以下简称为 Lua 数组),并详细阐述其核心操作,助你打下坚实的 Lua 基础。
一、 Lua Table 的本质:数组与哈希表的统一
理解 Lua 数组操作的前提是明白 Lua table
的本质。与其他许多语言不同,Lua 没有内置专门的 array
类型。table
是 Lua 中唯一的数据结构构造器,它是一种关联数组(Associative Array)。这意味着 table
可以用任何非 nil
的 Lua 值(包括数字、字符串、布尔值、函数甚至其他 table)作为键(key),并将这些键映射到相应的值(value)。
当我们谈论在 Lua 中使用“数组”时,我们特指一种 table
的特定使用模式:主要使用连续的正整数(通常从 1 开始)作为键来存储一系列值。Lua 内部对这种使用模式进行了优化。当一个 table
的键主要是从 1 开始的连续整数时,Lua 会尝试使用类似传统数组的内存布局(数组部分,array part)来存储这些值,以提高访问效率。同时,对于非整数键或者不连续的整数键,Lua 会使用哈希表(哈希部分,hash part)来存储。
这种设计的优点在于其极高的灵活性。你可以在同一个 table
中混合使用整数键和非整数键,甚至创建“稀疏”数组(即整数键不连续)。但为了有效地模拟和操作传统数组,我们通常遵循以下约定:
- 使用正整数作为索引(键)。
- 索引从 1 开始。 这是 Lua 的标准约定,与 C/C++/Java/Python 等语言从 0 开始不同。
- 保持索引的连续性。 尽量避免在序列中间创建
nil
值“空洞”,因为这会影响长度操作符#
和ipairs
迭代器的行为。
二、 创建 Lua 数组
创建 Lua 数组主要通过 table
的构造器 {}
来完成。
1. 创建空数组:
lua
local emptyArray = {}
print("空数组长度:", #emptyArray) -- 输出: 空数组长度: 0
2. 创建带初始值的数组:
可以在构造器内直接列出元素,它们会自动被赋予从 1 开始的连续整数键。
```lua
local fruits = {"Apple", "Banana", "Orange"}
print("第一个水果:", fruits[1]) -- 输出: 第一个水果: Apple
print("数组长度:", #fruits) -- 输出: 数组长度: 3
local numbers = {10, 20, 30, 40, 50}
print("第三个数字:", numbers[3]) -- 输出: 第三个数字: 30
print("数组长度:", #numbers) -- 输出: 数组长度: 5
```
3. 混合类型数组:
Lua 数组的元素可以是任何类型,并且可以在同一个数组中混合使用。
lua
local mixedArray = {1, "hello", true, print, { nested = true }}
print("第二个元素:", mixedArray[2]) -- 输出: 第二个元素: hello
print("第五个元素类型:", type(mixedArray[5])) -- 输出: 第五个元素类型: table
print("数组长度:", #mixedArray) -- 输出: 数组长度: 5
4. (较少用于纯数组) 指定键值对创建:
虽然主要用于创建字典或稀疏结构,但也可以显式指定整数键。
lua
local specificIndices = { [1] = "one", [3] = "three" }
print(specificIndices[1]) -- 输出: one
print(specificIndices[2]) -- 输出: nil (因为没有指定索引 2)
print(specificIndices[3]) -- 输出: three
-- 注意:此时 #specificIndices 的行为可能不符合预期,因为它不是一个严格的序列
-- 在 Lua 5.1/5.2 中,# 遇到非整数键或 nil 空洞时行为未定义或可能给出非预期结果
-- 在 Lua 5.3+ 中,# 只计算序列部分(从1开始到第一个nil整数键之前)
-- 对于上面这个例子,在 Lua 5.3+ 中 #specificIndices 会是 1
最佳实践: 对于模拟数组,始终使用 {elem1, elem2, ...}
的形式或后续添加元素的方式来确保索引从 1 开始且连续。
三、 访问和修改数组元素
访问和修改 Lua 数组元素使用方括号 []
语法,并提供 1 基的整数索引。
1. 访问元素:
```lua
local colors = {"Red", "Green", "Blue"}
local firstColor = colors[1] -- firstColor = "Red"
local secondColor = colors[2] -- secondColor = "Green"
local thirdColor = colors[3] -- thirdColor = "Blue"
print(firstColor, secondColor, thirdColor) -- 输出: Red Green Blue
-- 访问不存在的索引会返回 nil
local nonExistent = colors[4]
print("不存在的元素:", nonExistent) -- 输出: 不存在的元素: nil
print(colors[0]) -- 输出: nil (索引从 1 开始)
```
2. 修改元素:
通过赋值语句可以直接修改指定索引处的值。
```lua
local planets = {"Mercury", "Venus", "Earth", "Mars"}
print("原始第三颗行星:", planets[3]) -- 输出: 原始第三颗行星: Earth
planets[3] = "Terra" -- 修改索引为 3 的元素
print("修改后的第三颗行星:", planets[3]) -- 输出: 修改后的第三颗行星: Terra
-- 如果索引超出了当前数组末尾,赋值操作会扩展数组(可能产生 nil 空洞)
planets[5] = "Jupiter"
print("第五颗行星:", planets[5]) -- 输出: 第五颗行星: Jupiter
print("当前数组长度:", #planets) -- 输出: 当前数组长度: 5 (Lua 5.3+ 会正确计算到 5)
print(planets[4]) -- 输出: Mars (索引 4 仍然存在)
```
注意: 访问或修改时务必注意 Lua 的 1 基索引约定。
四、 获取数组长度
获取 Lua 数组(严格来说是序列)的长度,最常用的方法是使用一元长度操作符 #
。
```lua
local letters = {'a', 'b', 'c', 'd'}
local len = #letters
print("数组 'letters' 的长度:", len) -- 输出: 数组 'letters' 的长度: 4
local empty = {}
print("空数组长度:", #empty) -- 输出: 空数组长度: 0
```
#
操作符的重要限制:
#
操作符在 Lua 5.1/5.2 中对于包含 nil
值“空洞”或非正整数键的 table
行为是未定义的。从 Lua 5.3 开始,#
操作符的行为被明确定义为:返回序列的长度,即从索引 1 开始到第一个 nil
值整数键之前的最后一个整数键。
```lua
local sequence = {10, 20, 30}
print(#sequence) -- 输出: 3
local withNilHole = {10, nil, 30}
print(#withNilHole) -- 在 Lua 5.3+ 中输出: 1 (因为索引 2 是 nil)
-- 在 Lua 5.1/5.2 中行为未定义,可能为 1 或 3
local sparse = {[1] = 10, [3] = 30}
print(#sparse) -- 在 Lua 5.3+ 中输出: 1 (索引 2 是隐式的 nil)
-- 在 Lua 5.1/5.2 中行为未定义
local mixed = {"a", "b", key = "value"}
print(#mixed) -- 输出: 2 (只计算序列部分)
```
结论: 为了让 #
操作符可靠地返回你期望的数组元素数量,必须确保你的数组是一个从 1 开始且没有 nil
值空洞的连续整数索引序列。如果需要处理可能包含 nil
或非序列的 table
大小,你需要使用 pairs
迭代并手动计数,或者使用 table.maxn
(Lua 5.2+,返回最大的正整数键)。
lua
-- 获取包含 nil 空洞或非序列 table 的最大索引 (Lua 5.2+)
local t = {[1]=1, [5]=5, [-1]=-1, ["key"]="value"}
print(table.maxn(t)) -- 输出: 5
五、 添加元素
向 Lua 数组添加元素主要有两种方式:在末尾追加和在指定位置插入。
1. 在末尾追加元素:
-
方法一:直接赋值给
#t + 1
这是传统且高效的方式。利用#
获取当前长度,然后将新元素赋值给下一个索引。```lua
local tasks = {"Read email", "Write report"}
print("原始任务:", table.concat(tasks, ", ")) -- 输出: 原始任务: Read email, Write report
print("原始长度:", #tasks) -- 输出: 原始长度: 2tasks[#tasks + 1] = "Attend meeting"
print("追加后任务:", table.concat(tasks, ", ")) -- 输出: 追加后任务: Read email, Write report, Attend meeting
print("追加后长度:", #tasks) -- 输出: 追加后长度: 3
``` -
方法二:使用
table.insert(t, value)
这是 Lua 标准库提供的函数,专门用于在序列的末尾添加元素。它更具可读性,并且在某些 Lua 实现或 JIT 编译器下可能经过特别优化。lua
local items = {100, 200}
table.insert(items, 300) -- 等价于 items[#items + 1] = 300
print("追加后 items:", table.concat(items, ", ")) -- 输出: 追加后 items: 100, 200, 300
print("追加后长度:", #items) -- 输出: 追加后长度: 3
2. 在指定位置插入元素:
使用 table.insert(t, pos, value)
可以在数组的指定位置 pos
插入一个新元素 value
。原来在 pos
位置及之后的所有元素都会向后移动一位。
```lua
local letters = {'a', 'c', 'd'}
print("原始 letters:", table.concat(letters, "")) -- 输出: 原始 letters: acd
print("原始长度:", #letters) -- 输出: 原始长度: 3
-- 在索引 2 的位置插入 'b'
table.insert(letters, 2, 'b')
print("插入后 letters:", table.concat(letters, "")) -- 输出: 插入后 letters: abcd
print("插入后长度:", #letters) -- 输出: 插入后长度: 4
-- 在开头插入 '!' (索引 1)
table.insert(letters, 1, '!')
print("再插入后 letters:", table.concat(letters, "")) -- 输出: 再插入后 letters: !abcd
print("再插入后长度:", #letters) -- 输出: 再插入后长度: 5
-- 在末尾插入 'e' (等价于 table.insert(letters, 'e') 或 letters[#letters+1]='e')
table.insert(letters, #letters + 1, 'e')
print("末尾插入后 letters:", table.concat(letters, "")) -- 输出: 末尾插入后 letters: !abcde
print("末尾插入后长度:", #letters) -- 输出: 末尾插入后长度: 6
```
性能考量: 在数组中间插入元素 (table.insert(t, pos, value)
当 pos <= #t
) 通常比在末尾追加 (table.insert(t, value)
或 t[#t+1]=value
) 代价更高,因为它需要移动后续的所有元素。对于非常大的数组和频繁的中间插入操作,性能影响可能比较明显。
六、 删除元素
从 Lua 数组中删除元素同样有两种主要场景:删除末尾元素和删除指定位置元素。
1. 删除末尾元素:
使用 table.remove(t)
可以高效地移除并返回数组 t
的最后一个元素。
```lua
local numbers = {1, 2, 3, 4, 5}
print("原始 numbers:", table.concat(numbers, ", ")) -- 输出: 原始 numbers: 1, 2, 3, 4, 5
print("原始长度:", #numbers) -- 输出: 原始长度: 5
local removedValue = table.remove(numbers)
print("被移除的元素:", removedValue) -- 输出: 被移除的元素: 5
print("删除后 numbers:", table.concat(numbers, ", ")) -- 输出: 删除后 numbers: 1, 2, 3, 4
print("删除后长度:", #numbers) -- 输出: 删除后长度: 4
```
2. 删除指定位置元素:
使用 table.remove(t, pos)
可以移除并返回数组 t
在指定位置 pos
的元素。原来在 pos
之后的所有元素都会向前移动一位来填补空缺。
```lua
local colors = {"Red", "Green", "Blue", "Yellow"}
print("原始 colors:", table.concat(colors, ", ")) -- 输出: 原始 colors: Red, Green, Blue, Yellow
print("原始长度:", #colors) -- 输出: 原始长度: 4
-- 删除索引为 2 的元素 ("Green")
local removedColor = table.remove(colors, 2)
print("被移除的颜色:", removedColor) -- 输出: 被移除的颜色: Green
print("删除后 colors:", table.concat(colors, ", ")) -- 输出: 删除后 colors: Red, Blue, Yellow
print("删除后长度:", #colors) -- 输出: 删除后长度: 3
```
性能考量: 与插入类似,删除数组中间的元素 (table.remove(t, pos)
当 pos < #t
) 的成本通常高于删除末尾元素 (table.remove(t)
), 因为它也需要移动后续元素。
重要警告:避免使用 t[pos] = nil
来“删除”数组元素
虽然可以将数组中某个索引的值设为 nil
,但这并不是推荐的删除序列元素的方式。这样做会在数组中创建一个 nil
空洞,破坏其序列的连续性。
```lua
local data = {10, 20, 30, 40}
data[2] = nil -- 错误的方式 "删除" 元素
print(data[1], data[2], data[3], data[4]) -- 输出: 10 nil 30 40
-- 后果:
-- 1. #data 的行为变得不可靠 (Lua 5.3+ 会返回 1)
print("长度 (#):", #data) -- Lua 5.3+ 输出: 1
-- 2. ipairs 迭代会提前停止
for i, v in ipairs(data) do
print("ipairs:", i, v) -- 只会输出: ipairs: 1 10
end
-- 3. 其他依赖序列连续性的操作可能失败
-- 例如 table.concat 会在 nil 处停止
print(table.concat(data, ",")) -- 输出: 10 (Lua 5.3+) 或 报错 (Lua 5.1/5.2)
```
正确做法: 始终使用 table.remove(t, pos)
来删除序列中的元素,它会保持数组的紧凑和连续性。
七、 遍历数组
遍历 Lua 数组(序列)是常见的操作,主要有两种推荐的方法:
1. 使用数字 for
循环和 #
操作符:
这是最通用、最直接且通常性能最好的遍历序列的方式。它依赖于 #
操作符正确返回序列长度。
lua
local scores = {95, 88, 72, 100}
print("遍历 scores (数字 for):")
for i = 1, #scores do
local score = scores[i]
print("索引:", i, " 值:", score)
end
-- 输出:
-- 索引: 1 值: 95
-- 索引: 2 值: 88
-- 索引: 3 值: 72
-- 索引: 4 值: 100
这种方法的好处是:
* 明确控制索引范围 (1 到 #t
)。
* 即使数组在循环内部被修改(例如,添加元素),只要 #scores
在循环开始时确定,循环次数就是固定的(除非你修改循环变量 i
或使用 break
)。但要注意,如果在循环中删除元素,可能会跳过某些元素或访问 nil
。
2. 使用 ipairs
迭代器:
ipairs
是专门为遍历序列(从索引 1 开始的连续整数键)设计的迭代器。它会依次返回索引和值,直到遇到第一个 nil
值的整数键。
lua
local weekdays = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"}
print("遍历 weekdays (ipairs):")
for index, day in ipairs(weekdays) do
print("索引:", index, " 值:", day)
end
-- 输出:
-- 索引: 1 值: Monday
-- 索引: 2 值: Tuesday
-- 索引: 3 值: Wednesday
-- 索引: 4 值: Thursday
-- 索引: 5 值: Friday
ipairs
的优点:
* 语法简洁、语义清晰,明确表示正在遍历一个序列。
* 保证按索引 1, 2, 3... 的顺序遍历。
* 对于严格的序列(无 nil
空洞),ipairs
和数字 for
循环效果相同。
ipairs
的限制:
* 如前所述,它会在第一个 nil
整数键处停止。如果你的 table
不是严格的序列,ipairs
可能不会遍历所有你期望的元素。
对比 pairs
迭代器:
Lua 还提供了 pairs
迭代器,它可以遍历 table
中所有的键值对,包括非整数键和不连续的整数键。pairs
不保证遍历顺序。因此,对于需要按顺序处理的数组(序列),应该使用数字 for
循环或 ipairs
,而不是 pairs
。
lua
local mixedTable = {[1]="one", [3]="three", key="value"}
print("遍历 mixedTable (pairs):")
for k, v in pairs(mixedTable) do
print("键:", k, " 值:", v) -- 输出顺序不保证
end
-- 可能输出 (顺序不定):
-- 键: 1 值: one
-- 键: key 值: value
-- 键: 3 值: three
八、 其他常用数组操作
除了上述核心操作,table
库还提供了一些其他有用的函数,可以应用于数组(序列)。
1. 数组排序:table.sort(t, [comp])
对数组 t
进行原地排序。默认按 Lua 的标准比较规则(数字按大小,字符串按字母顺序)升序排列。可以提供一个可选的比较函数 comp(a, b)
,该函数在 a
应该排在 b
前面时返回 true
。
```lua
local numbers = {5, 1, 4, 2, 3}
table.sort(numbers)
print("排序后 numbers:", table.concat(numbers, ", ")) -- 输出: 排序后 numbers: 1, 2, 3, 4, 5
local names = {"Charlie", "Alice", "Bob"}
table.sort(names)
print("排序后 names:", table.concat(names, ", ")) -- 输出: 排序后 names: Alice, Bob, Charlie
-- 自定义降序排序
local scores = {80, 100, 90}
table.sort(scores, function(a, b) return a > b end)
print("降序排序后 scores:", table.concat(scores, ", ")) -- 输出: 降序排序后 scores: 100, 90, 80
-- 对包含 table 的数组排序
local persons = { {name="Bob", age=30}, {name="Alice", age=25} }
table.sort(persons, function(a, b) return a.age < b.age end) -- 按年龄升序
print("按年龄排序后:")
for i, p in ipairs(persons) do print(p.name, p.age) end
-- 输出:
-- 按年龄排序后:
-- Alice 25
-- Bob 30
```
注意: table.sort
是原地修改,不会返回新的排序后的数组。它同样要求 t
是一个没有 nil
空洞的序列。
2. 数组连接成字符串:table.concat(t, [sep], [i], [j])
将数组 t
的元素(假定都是字符串或数字)连接成一个单一的字符串。
* sep
(可选): 元素之间的分隔符字符串,默认为空字符串 ""
。
* i
(可选): 开始连接的索引,默认为 1。
* j
(可选): 结束连接的索引,默认为 #t
。
```lua
local parts = {"Lua", "is", "awesome"}
local sentence = table.concat(parts, " ")
print(sentence) -- 输出: Lua is awesome
local csvData = {1, 2, 3, 4, 5}
local csvString = table.concat(csvData, ",")
print(csvString) -- 输出: 1,2,3,4,5
-- 连接子序列
local letters = {'a', 'b', 'c', 'd', 'e'}
local sub = table.concat(letters, "", 2, 4) -- 连接索引 2 到 4 ('b', 'c', 'd')
print(sub) -- 输出: bcd
```
注意: table.concat
要求数组元素是字符串或数字(会自动转换成字符串)。如果遇到 nil
或其他类型,行为可能因 Lua 版本而异(早期版本可能报错,较新版本可能在 nil
处停止)。
3. 数组(序列)的复制:
Lua 中 table
是引用类型。简单的赋值 newArray = oldArray
只会创建对同一个 table
的引用,修改 newArray
也会影响 oldArray
。要创建数组的独立副本,需要手动遍历并复制元素。
-
浅拷贝 (Shallow Copy): 只复制数组本身,如果元素是
table
或其他引用类型,则复制的是引用。```lua
local original = {1, 2, { nested = true }}
local shallowCopy = {}
for i = 1, #original do
shallowCopy[i] = original[i]
endshallowCopy[1] = 100 -- 修改副本的值类型元素,不影响原始
shallowCopy[3].nested = false -- 修改副本中嵌套 table 的内容,会影响原始print(original[1]) -- 输出: 1
print(original[3].nested) -- 输出: false
``` -
深拷贝 (Deep Copy): 递归地复制所有嵌套的
table
,创建完全独立的副本。这通常需要一个递归函数。```lua
function deepcopy(orig)
local orig_type = type(orig)
local copy
if orig_type == 'table' then
copy = {}
for orig_key, orig_value in pairs(orig) do
copy[deepcopy(orig_key)] = deepcopy(orig_value) -- 递归复制键和值
end
setmetatable(copy, deepcopy(getmetatable(orig))) -- 复制元表
else -- number, string, boolean, nil, function, userdata, thread
copy = orig
end
return copy
endlocal original = {1, 2, { nested = true }}
local deepCopy = deepcopy(original)deepCopy[1] = 100
deepCopy[3].nested = falseprint(original[1]) -- 输出: 1
print(original[3].nested) -- 输出: true (深拷贝后,修改副本不影响原始的嵌套 table)
```
选择浅拷贝还是深拷贝取决于你的具体需求。对于只包含值类型(数字、字符串、布尔)的简单数组,浅拷贝通常就足够了。
九、 总结与最佳实践
掌握 Lua 中 table
作为数组的核心操作是编写高效、可靠 Lua 代码的关键。回顾一下要点和最佳实践:
- 理解基础: Lua 没有专门的数组类型,使用
table
并通过连续的、从 1 开始的整数键来模拟数组(序列)。 - 创建: 使用
{}
构造器,{val1, val2, ...}
是创建序列的最直接方式。 - 访问与修改: 使用
t[index]
,牢记 1 基索引。 - 长度: 使用
#t
获取序列长度,但要确保数组是连续的(无nil
空洞),否则#
的行为可能不符合预期(尤其在 Lua 5.3 之前)。 - 添加:
- 末尾追加:
t[#t + 1] = value
或table.insert(t, value)
。 - 指定位置插入:
table.insert(t, pos, value)
,注意性能影响。
- 末尾追加:
- 删除:
- 末尾删除:
table.remove(t)
(高效)。 - 指定位置删除:
table.remove(t, pos)
,注意性能影响。 - 绝对避免 使用
t[pos] = nil
来删除序列元素,它会破坏序列结构。
- 末尾删除:
- 遍历:
- 首选数字
for
循环 (for i = 1, #t do ...
),通用且高效。 ipairs(t)
用于按顺序遍历序列,语法简洁,但在nil
空洞处停止。- 避免 使用
pairs(t)
遍历需要保证顺序的数组。
- 首选数字
- 标准库函数: 善用
table.sort
(原地排序) 和table.concat
(连接成字符串)。 - 复制: 理解浅拷贝和深拷贝的区别,根据需要选择合适的复制方式。
- 保持序列性: 为了可靠地使用
#
、ipairs
、table.sort
、table.concat
等,尽量维护数组的序列性(1 基、连续整数索引、无nil
空洞)。
Lua 的 table
虽是一个统一的结构,但通过遵循这些约定和熟练运用核心操作,你可以像使用其他语言中的专用数组一样高效地处理序列数据,同时还能享受到 table
带来的无与伦比的灵活性。深入理解并实践这些操作,将为你驾驭更复杂的 Lua 编程打下坚实的基础。