Lua 中的数组是以下标从 1 开始的连续 table:Lua 没有独立的数组类型,而是用 table 的整数键部分模拟数组,配合 # 长度运算符和 table 标准库完成增删改查;迭代器则是基于泛型 for 协议的函数,通过反复调用逐一产出集合中的元素,是 Lua 遍历一切数据结构(数组、哈希表、自定义容器)的统一机制。
table 作为数组
Lua 中的数组是一种顺序集合,可以包含不同数据类型的元素。Lua 数组的索引从 1 开始(这是与 C、Python 等语言最明显的差异之一),大小是动态的,可以随时增长和缩小。
字面量构造与 # 长度运算符
-- 字面量构造:未显式指定键时,元素依次获得 1, 2, 3... 的整数键
local a = {'lua', 'js', 'c++', 'python', 'java', 'docker'}
print(#a) --> 6
for i = 1, #a do
print(a[i])
end
# 是长度运算符,用于获取数组的「长度」。但需要注意:# 返回的并不是「元素个数」,而是 table 的某一个「边界」(border)——即满足 t[n] ~= nil 且 t[n+1] == nil 的某个 n。
# 的边界陷阱
当数组中间存在 nil(形成「空洞」)时,# 的结果是不可靠的,它可能返回任意一个边界:
local t = {1, 2, 3, nil, 5}
print(#t) -- 可能是 3,也可能是 5,取决于实现,不要依赖
实践原则:只对无空洞的序列(sequence)使用 #。如果数组中可能出现 nil,要么显式维护一个长度字段(如 t.n),要么用 table.pack(它会把长度存在 n 字段中):
local t = table.pack(1, 2, nil, 4)
print(t.n) --> 4,真实元素个数
for i = 1, t.n do
print(t[i]) -- 第 3 个是 nil,也能完整遍历
end
多维数组
多维数组就是「table 的 table」:
local a = {}
for i = 1, 3 do
a[i] = {}
for j = 1, 3 do
a[i][j] = i * j
end
end
for i = 1, 3 do
for j = 1, 3 do
io.write(a[i][j], " ")
end
print()
end
table 标准库常用函数
table.insert:在指定位置插入元素,默认追加到末尾。
local t = {1, 2, 3}
table.insert(t, 4) --> t = {1, 2, 3, 4}
table.insert(t, 1, 0) --> t = {0, 1, 2, 3, 4},在位置 1 插入,后续元素后移
table.remove:删除指定位置的元素(默认最后一个),返回被删元素,后续元素前移。
local last = table.remove(t) --> 移除并返回 4
local first = table.remove(t, 1) --> 移除并返回 0
注意:table.insert/remove 在中间位置操作时需要移动后续所有元素,时间复杂度为 O(n),频繁操作大数组时应改用「栈式」的尾部增删。
table.sort:原地排序,支持自定义比较函数。
local t = {5, 2, 8, 1}
table.sort(t) --> {1, 2, 5, 8},默认升序
table.sort(t, function(a, b) --> {8, 5, 2, 1},自定义降序
return a > b
end)
table.concat:把数组中的字符串(或数字)拼接成一个字符串。
local parts = {"hello", "lua", "world"}
print(table.concat(parts, ", ")) --> hello, lua, world
table.concat 是构建大字符串的高效方式,远优于在循环中用 .. 反复拼接,这一点在 Lua 性能优化实践 中有更详细的讨论。
table.unpack:把数组展开为多个返回值(Lua 5.1 中是全局函数 unpack)。
local t = {10, 20, 30}
print(table.unpack(t)) --> 10 20 30
local a, b = table.unpack(t) -- a = 10, b = 20
迭代器深入:泛型 for 的三要素
迭代器用于遍历集合中的元素。Lua 的泛型 for 语句完整形式是:
for 变量列表 in 迭代函数, 状态, 控制变量 do ... end
每次循环,Lua 都会调用 迭代函数(状态, 控制变量),把返回值赋给变量列表,并用第一个返回值更新控制变量;当第一个返回值为 nil 时循环结束。
以内置的 ipairs 为例:
local array = {"Google", "Nhooo"}
for key, value in ipairs(array) do
print(key, value)
end
-- 等价于:反复调用 iter(array, key),key 从 0 开始
pairs vs ipairs vs 自定义迭代
- ipairs:只遍历整数键
1, 2, 3...,遇到第一个nil就停止。顺序严格递增。 - pairs:遍历 table 中的所有键(包括字符串键和空洞之后的整数键),但遍历顺序不保证(取决于哈希实现)。
- 自定义迭代器:执行顺序和终止条件完全由你定义,适合遍历非 table 的数据源(文件行、链表、数据库游标等)。
local t = {10, 20, nil, 40, name = "demo"}
for i, v in ipairs(t) do print("ipairs:", i, v) end
-- 只输出 1:10, 2:20,遇到 t[3] 为 nil 停止,name 字段也不会出现
for k, v in pairs(t) do print("pairs:", k, v) end
-- 输出全部 4 个键值对(含 name = "demo"),顺序不定
自定义迭代器
无状态迭代器
无状态迭代器不保存任何内部状态,只依赖「状态」和「控制变量」两个入参。下面的 square 迭代器产出数字和它的平方:
function square(iteratorMaxCount, currentNumber)
if currentNumber < iteratorMaxCount then
currentNumber = currentNumber + 1
return currentNumber, currentNumber * currentNumber
end
-- 返回 nil(不写 return)时,泛型 for 结束循环
end
for i, n in square, 3, 0 do
print(i, n)
end
-- 输出:1 1 / 2 4 / 3 9
这里 3 是状态(迭代上限),0 是初始控制变量。迭代器协议的返回值约定很简单:第一个返回值是 nil 就结束遍历,否则继续。
用闭包实现多状态迭代器
当无状态迭代器的「状态 + 控制变量」不够用时,可以用闭包把状态保存在上值(upvalue)里:
local array = {"Google", "Nhooo"}
function elementIterator(collection)
local index = 0
local count = #collection
return function() -- 闭包捕获 index 和 count
index = index + 1
if index <= count then
return collection[index]
end
end
end
for element in elementIterator(array) do
print(element)
end
链表遍历示例
自定义迭代器最大的价值在于遍历非 table 结构,例如单向链表:
-- 构造链表:1 -> 2 -> 3
local list
for _, v in ipairs({3, 2, 1}) do
list = {value = v, next = list}
end
-- 返回一个遍历链表节点的迭代器
local function listIter(node)
return function()
if node then
local current = node
node = node.next
return current.value
end
end
end
for v in listIter(list) do
print(v) --> 1 2 3
end
实用模式
用 table.sort 做复杂排序
比较函数接收两个元素,返回「第一个是否应排在前面」:
local players = {
{name = "alice", score = 90},
{name = "bob", score = 85},
{name = "carol", score = 90},
}
-- 先按分数降序,同分再按名字升序
table.sort(players, function(a, b)
if a.score ~= b.score then
return a.score > b.score
end
return a.name < b.name
end)
注意:比较函数必须满足严格弱序(comp(a, a) 恒为 false),否则 table.sort 可能报错 invalid order function for sorting。
用迭代器实现 filter / map
-- filter:产出满足条件的元素
local function filter(pred, t)
local i = 0
return function()
while true do
i = i + 1
local v = t[i]
if v == nil then return nil end
if pred(v) then return v end
end
end
end
-- map:对每个元素应用变换
local function map(f, t)
local i = 0
return function()
i = i + 1
if t[i] == nil then return nil end
return f(t[i])
end
end
local nums = {1, 2, 3, 4, 5, 6}
for v in filter(function(x) return x % 2 == 0 end, nums) do
io.write(v, " ") --> 2 4 6
end
print()
for v in map(function(x) return x * x end, nums) do
io.write(v, " ") --> 1 4 9 16 25 36
end
print()
常见陷阱
遍历时删除元素
在 pairs 遍历中删除当前键是安全的,但新增键会导致行为未定义;用数字 for 倒序删除数组元素则是常见安全做法:
local t = {1, 2, 3, 4, 5}
-- 倒序遍历删除偶数,正序删除会跳过元素
for i = #t, 1, -1 do
if t[i] % 2 == 0 then
table.remove(t, i)
end
end
print(table.concat(t, ",")) --> 1,3,5
# 在稀疏数组上的行为
如前文所述,{1, 2, nil, nil, 5} 这类稀疏数组上 # 的结果不可靠。稀疏数据应改用「键值对」思维(用 pairs 遍历),或者显式维护长度,不要依赖 #。
如果你刚开始学 Lua,建议先阅读 Lua 快速入门教程 建立整体认识;table 的更深层机制(如给数组加边界检查)则需要理解元表与元方法。
常见问题(FAQ)
Lua 数组下标为什么从 1 开始?
这是 Lua 的语言设计选择,主要面向非专业程序员和领域专家,1 起始更符合日常计数习惯(「第 1 个元素」就是下标 1)。table 本身并不限制下标,t[0] 甚至负数下标都合法,只是 # 和 ipairs 等约定俗成的工具都从 1 开始工作。
pairs 和 ipairs 有什么区别?
ipairs 只按 1, 2, 3… 的顺序遍历连续整数键,遇到第一个 nil 即停止;pairs 遍历 table 中的所有键(含字符串键和空洞后的整数键),但顺序不保证。遍历「数组部分」用 ipairs,遍历哈希表或混合结构用 pairs。
table 里有 nil 时 # 会怎样?
当数组中间存在 nil(空洞)时,# 会返回某一个满足 t[n] ~= nil 且 t[n+1] == nil 的边界 n,但具体是哪个边界由实现决定,结果不可靠。应改用 table.pack 的 n 字段或自行维护长度。
遍历时能删除元素吗?
可以,但要分情况:pairs 遍历中删除当前已访问的键是安全的,新增键则行为未定义;数组删除推荐用数字 for 倒序遍历配合 table.remove,正序删除会因为元素前移而跳过元素。
自定义迭代器必须返回几个值?
泛型 for 只要求「迭代函数、状态、控制变量」三要素。迭代函数每次被调用时返回的第一个值若为 nil,循环结束;否则第一个值成为新的控制变量,所有返回值依次赋给 for 的变量列表。返回一个值还是多个值完全由你决定。
相关阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。