🔥 本文专栏:Lua
🌸作者主页:努力努力再努力wz



💪 今日博客励志语录:
愿你既能在无人喝彩的日子里默默前进,也能在阳光灿烂的午后安心享受生活。
思维导图
text
C++ 容器:vector / list / map 的底层数据组织方式不同
↓
外部遍历不应该依赖每种容器的实现细节
↓
迭代器:将「如何找到下一个元素」封装起来
↓
Lua 同样需要遍历数据:Table、链表结构、匹配结果......
↓
数值 for:初始值、终止值、步长;适合数值规律明确的循环
↓
如果无法通过简单的索引递增取得下一项呢?
↓
Lua 使用函数实现迭代器:每次调用取得下一组数据
↓
问题:多次调用之间,遍历位置由谁维护?
│
├── 有状态迭代器:闭包捕获局部变量,自己记住位置
│ └── 词法作用域 → Upvalue → 返回函数仍能访问状态
│
└── 无状态迭代器:外部维护和传递位置
└── 泛型 for:iterator、state、control
↓
泛型 for 初始化一次,随后反复调用真正的迭代器函数
↓
├── ipairs(t) → 迭代器函数、t、0
│ 1、2、3......遇到 nil 停止
│
└── pairs(t) → next、t、nil(普通 Table)
利用上一个 Key 获取下一项
引入:为什么不直接从 pairs() 和 ipairs() 开始?
学习 Lua 的过程中,我们很快就会遇到这样的代码:
lua
local t = {10, 20, 30}
for i, v in ipairs(t) do
print(i, v)
end
如果仅仅从语法层面理解,无非就是「ipairs() 用来遍历数组,pairs() 用来遍历 Table」。但这样会留下几个疑问:
in后面为什么能直接放一个函数调用?ipairs(t)只执行一次,为什么后面可以不断得到新元素?for i, v为什么能接收到两个值?数量一定是两个吗?- 迭代器既然是函数,多次调用之间又是谁记住了上一次的位置?
pairs()为什么不需要按照数值索引递增,也能遍历一张 Table?
其实,这些问题背后是一条完整的逻辑链:迭代器为什么存在 → Lua 如何实现迭代器 → 遍历状态如何维护 → 泛型 for 如何驱动迭代器 → ipairs() / pairs() 为什么能工作。
我们沿着这条链路逐步展开。
一、从熟悉的 C++ 迭代器出发:它究竟解决了什么问题?
1. 不同容器,内部组织数据的方式不同
在之前学习 C++ STL 容器时,我们已经认识过迭代器(Iterator)。
例如,vector 通常使用连续内存,list 使用链表,而 map 通常使用平衡树(典型实现是红黑树)。它们的底层数据结构不同,寻找下一个元素的方式自然也不一样。
- 对连续数组而言,可以通过索引递增或地址偏移寻找下一项。
- 对链表而言,需要沿着节点指针寻找下一个节点。
- 对树结构而言,需要遵循树的中序后继等规则寻找下一个元素。
如果外部每次遍历时,都要先研究容器内部究竟是数组、链表还是树,那么上层代码就与底层实现紧密耦合了。
所以问题在于:能不能把「如何找到下一个元素」这件事交给容器自身实现,而上层只使用一套统一的访问方式?

2. C++ 迭代器的核心意义是解耦
例如:
cpp
std::vector<int> nums = {10, 20, 30};
for (auto it = nums.begin(); it != nums.end(); ++it) {
std::cout << *it << std::endl;
}
对外部代码而言,只需要理解:
text
begin() / end() 获取遍历起点与结束位置
*it 访问当前位置的元素
++it 移动到下一个位置
it != end() 判断是否结束
++it 究竟是对指针做加法、沿链表寻找下一个节点,还是在树中寻找后继节点,都可以由具体的迭代器实现负责。
我们之前模拟实现过容器,知道迭代器可以是一个类,内部保存指针或其他位置信息,再重载 *、++ 等运算符。容器也可以通过类型别名向外暴露自己的迭代器类型。不过并非所有 C++ 迭代器都一定是类对象,原生指针同样可以作为迭代器。
这与我们以前理解 Linux「一切皆文件」的统一访问接口,有相似的设计思想:上层使用统一接口,下层根据具体对象提供不同实现。两者不是同一套机制,但都体现了解耦。
因此先记住一句话:
迭代器的核心意义,是将数据的组织方式与外部遍历方式解耦,为外部提供一种统一地获取下一个元素的机制。
二、Lua 为什么同样需要迭代器?先看数值 for 的局限
1. 数值 for 通过固定的数值规律控制循环
我们之前已经学习过 Lua 的数值 for:
lua
for i = 1, 5, 1 do
print(i)
end
这里有三个控制表达式,分别表示:初始值、终止边界、步长。进入循环时,它们会各自求值一次,然后按照数值递增或递减的规则执行。
如果需要遍历一个连续整数索引的 Table,这种方式当然可行:
lua
local t = {10, 20, 30}
for i = 1, #t do
print(t[i])
end
但是这里已经隐含一个条件:我们要能通过 1、2、3...... 这样的整数索引连续取得需要的数据。#t 对存在空洞的 Table 也不能简单理解成「所有元素的数量」;在存在多个长度边界时,结果不一定是我们直觉中的最后一个非 nil 索引。
2. Table 并不只有连续整数键
例如:
lua
local player = {
name = "wangz",
level = 10,
hp = 100
}
这里的键是字符串,不存在一种简单的 i = i + 1 规则,能够直接获取下一组键值对。
之前我们还学习过 Table 的底层存储:Lua 5.4 的 Table 通常将元素组织到**数组部分(TValue 数组)**与 Hash 部分(Node 数组)。这两部分的访问方式也不一样。
那么,一个自然的问题就出现了:
如果不同数据来源获取下一项的方式不同,难道每次都要让上层循环代码亲自处理这些差异吗?
答案当然是没有必要。Lua 同样可以引入迭代器来封装这些规则。
三、Lua 的迭代器是什么?它通常是一个函数
1. 先建立与 C++ 不同的关键认知
在 C++ 中,我们经常通过迭代器对象或原生指针进行遍历。而在 Lua 中,典型的迭代器是一个函数。
之前学习 Lua 函数时,我们已经知道函数是一种值,可以赋给变量,也可以作为返回值。因此,迭代器函数并不需要专门定义一个 Iterator 类。
先不要考虑泛型 for,我们可以手动实现一个最简单的迭代器:
lua
local t = {10, 20, 30}
local index = 0
local function iterator()
index = index + 1
return t[index]
end
print(iterator()) -- 10
print(iterator()) -- 20
print(iterator()) -- 30
print(iterator()) -- nil
这里每次调用 iterator(),它都会访问下一个元素。第四次访问 t[4],得到 nil。
可以发现,Lua 迭代器最基本的行为,就是每次被调用时提供下一项数据。
2. 迭代器函数与循环控制不是同一回事
这一点我们要提前区分:
- 迭代器函数:负责按照自己的规则获取下一个元素。
- 泛型 for:负责自动反复调用迭代器,并根据返回结果决定是否继续。
所以,迭代器本身可以脱离泛型 for 单独调用。泛型 for 只是把这些重复调用和终止判断帮我们完成了。
但是这里出现了一个新的问题:既然迭代器会被反复调用,那么它如何知道上一次遍历到了哪里?
四、遍历位置应该如何保存?从变量作用域逐步推导
1. 如果把 index 写在迭代器函数内部
先尝试:
lua
local t = {10, 20, 30}
local function iterator()
local index = 0
index = index + 1
return t[index]
end
print(iterator()) -- 10
print(iterator()) -- 10
print(iterator()) -- 10
问题在于:每次调用 iterator(),都会重新执行 local index = 0,所以每次都只访问 t[1]。
这里的关键并不是「局部变量不能保存状态」,而是局部变量的声明与初始化语句被反复执行了。
2. 那么把 index 定义成全局变量呢?
lua
local t = {10, 20, 30}
index = 0
function iterator()
index = index + 1
return t[index]
end
这当然能够工作。甚至我们可以给两个迭代器分别维护 index1 和 index2,并不是什么原理上做不到的事情。
但是这样会引入新的设计问题:
- 每增加一个独立迭代器,就要管理它对应的那一份状态。
- 多个函数如何准确地找到各自应使用的状态
- 全局变量暴露在全局环境中,其他代码也可能修改它,破坏遍历进度。
所以我们更希望:每个迭代器都有自己的遍历位置,而且这个位置不需要暴露为全局变量。
3. 外部局部变量也能保存状态
其实这样写就已经可以工作:
lua
local t = {10, 20, 30}
local index = 0
local function iterator()
index = index + 1
return t[index]
end
index 依然是局部变量,但其初始化语句不在反复执行的 iterator() 函数体中。iterator() 每次访问的是同一个变量,因此状态得以保留。
这里又引出了一个更好的设计:能不能每次创建迭代器时,自动创建一份独立的局部状态,再将它交给真正的迭代器函数使用?
这就需要我们之前学习过的函数返回值,以及 Lua 的闭包机制。
五、闭包:为什么返回的函数还能访问原来的局部变量?
1. 先从词法作用域理解变量为什么可见
来看一个我们已经熟悉的例子:
lua
local function outer()
local a = 100
local function inner()
print(a)
end
inner()
end
outer() -- 100
这里 a 是 outer() 内部的局部变量,但是 inner() 是在 a 可见的词法作用域内定义的,因此它同样可以访问 a。
这并不违背 local 的作用域规则。局部变量的词法作用域包含其中定义的函数体;不是说只能由声明它的那个函数中的直接语句访问。
2. 关键场景:把 inner 返回出去
lua
local function outer()
local a = 100
local function inner()
a = a + 1
return a
end
return inner
end
local f = outer()
print(f()) -- 101
print(f()) -- 102
print(f()) -- 103
我们按执行顺序分析:
第一步 :调用 outer(),创建局部变量 a = 100,再创建 inner 函数。
第二步 :return inner 返回的是函数值,并不是执行 inner() 的函数体。
第三步 :outer() 已经执行结束,但外部依然保存着返回的函数 f。
第四步 :多次调用 f(),发现结果依次是 101、102、103。
这说明:返回的函数继续引用了同一个 a,而且能够持续修改它。
如果只是每次复制一份 a = 100 的初始值,就不会出现这种持续递增的结果。
3. 词法作用域、Upvalue 和闭包分别解决什么问题?
- 词法作用域:解释内部函数为什么有权访问定义位置可见的外部局部变量。
- Upvalue(上值):内部函数捕获并关联的外部局部变量。
- 闭包(Closure):函数值与其引用的外部变量关联起来,使函数在原函数返回后仍能够继续访问这些变量。

这里不急着研究 Lua 虚拟机活动调用栈、变量如何存放以及垃圾回收实现。现阶段只需要掌握语言层面的结论:被闭包引用、仍然需要使用的局部变量,不会因为创建它的函数返回就直接失效。
4. 不是所有闭包都会共享同一份变量
lua
local function create()
local a = 100
return function()
a = a + 10
return a
end
end
local f1 = create()
local f2 = create()
print(f1()) -- 110
print(f1()) -- 120
print(f2()) -- 110
print(f2()) -- 120
这里每次调用 create(),都会创建一个新的局部变量 a,所以 f1 和 f2 分别维护自己的状态。
反过来,如果两个闭包捕获的是同一次函数调用创建的同一个局部变量,它们就可以共享这一份状态。
至此,我们就得到了一个非常适合实现迭代器的能力:返回一个函数,并让这个函数持续维护只属于它自己的遍历进度。
六、有状态迭代器:通过闭包让函数记住遍历位置
1. 不要混淆迭代器创建函数与真正的迭代器函数
lua
local function createIterator(t)
local index = 0
return function()
index = index + 1
return t[index]
end
end
这段代码里其实涉及两个函数。
createIterator(t) 是迭代器创建函数 。它负责初始化 index = 0,再返回一个能够引用 index 和 t 的匿名函数。
返回的匿名函数才是真正的迭代器函数 。它负责每次推进 index,再获取并返回下一个元素。
因此,不能把两者理解成「创建函数被反复调用」。对于一次遍历,我们通常只调用一次创建函数,之后反复调用的是它返回的闭包。
2. 手动调用验证
lua
local t = {10, 20, 30}
local it = createIterator(t)
print(it()) -- 10
print(it()) -- 20
print(it()) -- 30
print(it()) -- nil
createIterator(t) 创建了独立的 index,而返回的 it 一直在访问这同一个变量。
如果再次调用创建函数:
lua
local it1 = createIterator({10, 20, 30})
local it2 = createIterator({100, 200, 300})
print(it1()) -- 10
print(it1()) -- 20
print(it2()) -- 100
print(it1()) -- 30
print(it2()) -- 200
两次调用创建的是两份互不干扰的遍历状态。
3. 有状态迭代器可以直接交给泛型 for
lua
local t = {10, 20, 30}
for value in createIterator(t) do
print(value)
end
输出:
text
10
20
30
这里的关键是:
text
createIterator(t) → 只求值 / 调用一次,返回迭代器 it
it() → 第 1 次:10
it() → 第 2 次:20
it() → 第 3 次:30
it() → 第 4 次:nil,结束
为了理解,暂时可以近似写成:
lua
local it = createIterator(t)
while true do
local value = it()
if value == nil then
break
end
print(value)
end
这只是针对当前这个不使用传入参数的闭包迭代器的简化等效逻辑。后面我们会看到,泛型 for 的完整机制还会向迭代器传入两个参数。Lua 允许函数忽略多余的实参,所以这里仍然可以正常工作。
到这里,有状态迭代器的核心就是:由闭包自身维护跨调用的遍历进度。
七、无状态迭代器:不让函数自己保存位置,改由调用者传递
1. 为什么一定要让迭代器函数记住 index?
我们可以反过来考虑:既然迭代器每次都需要一个「上一次位置」,为什么不能让调用者保存它,然后在下一次调用时传进来?
于是,我们实现一个不需要闭包状态的函数:
lua
local function iterator(t, index)
index = index + 1
local value = t[index]
if value == nil then
return nil
end
return index, value
end
这里:
t是需要遍历的 Table。index是调用者传入的上一次位置。- 函数先计算下一个索引,再返回新的索引与对应的值。
与刚才不同:这个 iterator() 不需要自己跨调用保存 index。它每次只根据传入参数完成计算。
注意:无状态不等于整个遍历过程没有状态。只不过状态不保存在迭代器函数自身,而是由外部维护。
2. 先不使用泛型 for,手动维护状态
lua
local t = {10, 20, 30}
local index = 0
local i, v
i, v = iterator(t, index)
print(i, v) -- 1 10
index = i
i, v = iterator(t, index)
print(i, v) -- 2 20
index = i
i, v = iterator(t, index)
print(i, v) -- 3 30
这里每次都要把新返回的位置保存下来,再交给下一次调用。
换句话说:迭代器只管根据传入的位置找下一项,调用者负责保存和传递遍历进度。
3. 泛型 for 恰好能够自动承担这项工作
lua
local t = {10, 20, 30}
for i, v in iterator, t, 0 do
print(i, v)
end
输出:
text
1 10
2 20
3 30
这里 in 后面给出了三个值:
| 组成部分 | 示例 | 作用 |
|---|---|---|
| 迭代器函数 | iterator |
根据输入返回下一项 |
| 状态值 | t |
每次传给迭代器的被遍历对象 |
| 控制变量初始值 | 0 |
指定遍历从什么位置开始推进 |
于是,泛型 for 会调用:
text
iterator(t, 0) → 1, 10
iterator(t, 1) → 2, 20
iterator(t, 2) → 3, 30
iterator(t, 3) → nil
这里千万不要误以为迭代器要「返回更新后的 Table」。在当前例子里 t 始终是同一张 Table,变化的是控制变量。
这就解决了无状态迭代器如何在多次调用之间继续遍历的问题。
八、泛型 for 的通用运行机制:到底是谁反复调用了谁?
此前我们分别从有状态、无状态的角度理解了泛型 for。现在可以把它们统一起来。
1. 泛型 for 先初始化一次,再反复调用迭代器函数
Lua 泛型 for 的语法可以写成:
lua
for var1, var2, ... in 表达式列表 do
-- 循环体
end
其中,in 后面的表达式列表只在进入循环时求值一次,并提供迭代器函数等信息。之后重复执行的,是已经取得的迭代器函数。
所以:
lua
for value in createIterator(t) do
print(value)
end
与:
lua
local it = createIterator(t)
for value in it do
print(value)
end
都成立。前一种写法先调用创建函数得到迭代器;后一种直接给出已经创建好的迭代器函数。
这里要区分 「传入函数值」 与 「先调用一个函数得到函数值」,它们最终都能为泛型 for 提供真正的迭代器函数。
2. 泛型 for 在无状态场景中的通用执行模型
lua
for i, v in iterator, t, 0 do
print(i, v)
end
可以近似理解成:
lua
local state = t
local control = 0
local function iterator(t, index)
index = index + 1
local value = t[index]
if value == nil then
return nil
end
return index, value
end
while true do
local i, v = iterator(state, control)
if i == nil then
break
end
control = i
print(i, v)
end
注意,以上是为了说明语义而写的伪等价代码 ,不是 Lua 编译器真的把源码展开成了这段 while。

理解这一模型,就能得到泛型 for 的核心规则:
- 先取得迭代器函数、状态值与初始控制变量。
- 每次调用迭代器时,都传入
state和control。 - 将迭代器的返回值依次赋给循环变量。
- 第一个返回值还承担特殊职责:作为新的控制变量。
- 当第一个返回值为
nil时,结束循环;否则执行循环体,并继续调用。
这里也再次说明:有状态闭包迭代器同样符合泛型 for 语法。它可以忽略收到的 state、control 两个参数,直接使用自身捕获的变量。
3. for k, v 的两个变量是固定的吗?
不是。我们之前专门讨论过这个问题:循环变量的个数,并不是被固定成两个;它们按顺序接收迭代器函数的返回值。
例如,写一个返回三个值的迭代器:
lua
local function iterator3(t, index)
index = index + 1
local value = t[index]
if value == nil then
return nil
end
return index, value, value * 2
end
local t = {10, 20, 30}
for a, b, c in iterator3, t, 0 do
print(a, b, c)
end
输出:
text
1 10 20
2 20 40
3 30 60
这里 a、b、c 依次接收 index、value、value * 2。
- 声明的循环变量比返回值少,多出来的返回值会被忽略。
- 声明的循环变量比返回值多,缺少的部分会补
nil。 - 迭代器即使返回很多个值,泛型 for 始终根据第一个返回值判断是否结束,并将它作为下一次的控制变量。
所以,for k, v 之所以常见,并不是泛型 for 强制规定 Key、Value,而是 ipairs() 和 pairs() 所提供的迭代器,恰好返回了 Key 与 Value。
九、ipairs():为什么它只遍历从 1 开始的连续整数索引?
1. ipairs(t) 不是那个被反复调用的迭代器函数
lua
local t = {10, 20, 30}
for i, v in ipairs(t) do
print(i, v)
end
我们现在已经知道:ipairs(t) 在初始化时调用一次,它会返回三个值:
text
迭代器函数, t, 0
这正好满足我们之前学习的无状态迭代器机制。
2. 用 Lua 代码模拟 ipairs() 的核心思路
下面只是帮助理解的模拟版本,并不是 Lua 5.4 标准库的实际源码:
lua
local function ipairsIterator(t, index)
index = index + 1
local value = t[index]
if value == nil then
return nil
end
return index, value
end
local function myIpairs(t)
return ipairsIterator, t, 0
end
使用:
lua
local t = {10, 20, 30}
for i, v in myIpairs(t) do
print(i, v)
end
执行过程:
text
myIpairs(t) → 提供 ipairsIterator、t、0
ipairsIterator(t, 0) → 1, 10
ipairsIterator(t, 1) → 2, 20
ipairsIterator(t, 2) → 3, 30
ipairsIterator(t, 3) → nil,结束
这就是为什么 ipairs() 与无状态迭代器之间能够直接对应。
3. 为什么 ipairs() 不需要调用 #t?
lua
local t = {10, 20, 30, 40}
t[2] = nil
for i, v in ipairs(t) do
print(i, v)
end
输出:
text
1 10
原因不在于 #t 计算出来是多少,而在于:
text
索引 1 → t[1] = 10,继续
索引 2 → t[2] = nil,停止
ipairs() 不是先计算 Table 的长度,再循环到该长度 ;它直接从 1 递增,并在遇到第一个不存在的索引时结束。
4. "遍历整数索引"不等于"遍历底层数组部分"
这一点是之前学习 Table 底层存储时,特别容易混淆的。
lua
local t = {
[1] = 10,
[2] = 20,
[100] = 1000,
name = "wangz"
}
for i, v in ipairs(t) do
print(i, v)
end
这里只输出:
text
1 10
2 20
虽然 100 也是整数键,但是 t[3] 已经是 nil;ipairs() 不会跳过空洞去找 100,也不会访问字符串键 name。
更精确的表述应该是:
ipairs() 根据从 1 开始连续递增的整数索引访问 Table,并在第一个值为 nil 的位置停止;它并不是专门扫描底层 TValue 数组。
底层究竟把某个整数键放在数组部分还是 Hash 部分,属于实现细节,不应该和 ipairs() 的对外遍历规则混为一谈。
十、pairs() 与 next():如何遍历整张 Table?
1. pairs() 不只是遍历字符串键,而是遍历所有键值对
例如:
lua
local t = {
10, 20, 30,
name = "wangz",
level = 10
}
for k, v in pairs(t) do
print(k, v)
end
这里能够访问整数键 1、2、3,也能够访问字符串键 name、level。
但要注意:pairs() 不保证输出顺序,包括整数键的输出顺序也不做语言层面的保证。
2. 普通 Table 的 pairs(t) 返回什么?
对没有自定义 __pairs 元方法的普通 Table 而言,Lua 5.4 中:
lua
pairs(t)
相当于提供:
text
next, t, nil
因此:
lua
for k, v in pairs(t) do
print(k, v)
end
就可以写成:
lua
for k, v in next, t, nil do
print(k, v)
end
这里的 next 才是真正被泛型 for 反复调用的函数。
3. next() 为什么只需要上一次的 Key?
next() 的调用形式是:
lua
local nextKey, nextValue = next(t, key)
其中:
t是当前要遍历的 Table。key是上一次返回的 Key。- 返回值是下一组 Key 和 Value。
当第二个参数是 nil 时,代表从遍历起点开始。后面则持续将上一次的 Key 传回来。
因此,泛型 for 对普通 pairs(t) 所做的事情可以理解为:
text
next(t, nil) → 第一组 key1, value1
next(t, key1) → 下一组 key2, value2
next(t, key2) → 下一组 key3, value3
... → nil,结束

到这里,我们就能够得出一个结论:next() 不需要自己维护跨调用的遍历游标,因为上一次的 Key 已经被泛型 for 保存,并在下一次调用时重新传进来了。
4. 再联系 Table 的底层数组与 Hash 结构
既然之前已经学习过 Table 的两部分存储,我们自然还可以进一步推导 next() 的实现思路。
在 Lua 5.4 的 ltable.c 实现中,核心逻辑可以概括为两步。
第一步,根据上一次 Key 定位当前位置:
- 如果 Key 对应数组部分的整数索引,可以利用该索引定位。
- 如果 Key 位于 Hash 部分,需要根据 Key 查找对应节点;必要时会涉及哈希冲突链的查找。
第二步,从当前位置之后寻找下一个有效元素:
- 如果还处在数组部分,就从后续数组槽位继续寻找。
- 当进入 Hash 部分时,则按照 Node 数组的槽位顺序继续扫描,寻找下一个有效节点。
这里要修正一个容易误解的地方:冲突链主要用于根据 Key 找到对应的 Hash 节点,并不是说遍历 Hash 部分时会一直沿着冲突链去遍历所有元素。
因此,底层实现的整体思想可以写成:
text
泛型 for 保存上一次 Key
↓
next(t, key)
↓
根据 Key 找到原来的位置
↓
从该位置之后扫描下一项
↓
返回新 Key、Value
↓
泛型 for 更新 Key
这样我们就把之前学习的 Table 底层存储、Hash 定位、泛型 for 控制变量、无状态迭代器全部联系起来了。
不过这里仍需强调:以上是 Lua 5.4 具体实现的解释,而不是 pairs() 承诺的外部遍历顺序 。我们在编写业务代码时,不能根据数组与 Hash 的底层扫描顺序,假设 pairs() 一定按某种固定顺序输出。
十一、最后将整个知识体系串起来
1. 有状态迭代器与无状态迭代器
| 对比项 | 有状态迭代器 | 无状态迭代器 |
|---|---|---|
| 迭代器的典型形式 | 捕获变量的闭包函数 | 接收状态与控制变量的普通函数 |
| 遍历进度由谁维护 | 闭包捕获的局部变量 | 调用者(例如泛型 for) |
| 如何获取下一项 | 读取、修改自己的状态 | 根据传入的状态与位置计算 |
| 是否必须有创建函数 | 不一定,但工厂函数便于创建独立状态 | 不一定,可直接提供迭代器函数 |
| 示例 | createIterator(t) 返回闭包 |
iterator, t, 0 |
两者并不是说一个需要状态、另一个完全不需要状态。它们真正的区别是:遍历进度由谁保存、由谁传递。
2. ipairs() 与 pairs()
| 对比项 | ipairs(t) |
pairs(t)(普通 Table) |
|---|---|---|
| 返回的内容 | 迭代器函数、t、0 |
next、t、nil |
| 真正的迭代器 | 内置整数索引迭代函数 | next() |
| 控制变量 | 上一个整数索引 | 上一个 Key |
| 遍历规则 | 从 1 开始连续递增 | 遍历现存的各类键值对 |
| 停止条件 | 第一个索引值为 nil |
没有下一个键,返回 nil |
是否依赖 #t |
否 | 否 |
| 遍历顺序 | 1、2、3...... | 不保证 |
3. 最后形成一个统一的心智模型
我们现在可以这样理解 Lua 的整个迭代过程:
text
数据集合(Table / 自定义链表 / 匹配结果等)
↓
各自提供迭代器函数
↓
泛型 for 统一调用
↓
每次获得下一组返回值
↓
判断第一个返回值是否为 nil
↓
继续遍历或者结束
其中,迭代器函数负责如何获取下一个元素 ,泛型 for 负责如何重复调用并控制循环,而遍历位置既可以由闭包自己维护,也可以由泛型 for 通过控制变量维护。
所以,学到这里我们真正理解的,不只是 pairs() 和 ipairs() 这两个函数的用法,而是它们背后共同遵循的迭代器设计思想。
迭代器实现了遍历细节与循环控制的解耦;有状态迭代器通过闭包保存进度,无状态迭代器通过外部控制变量传递进度。泛型 for 则把这些不同的迭代器统一到一套循环语法中。
到这里,我们对于 Lua 泛型 for、闭包以及两种迭代器的认识就基本完整了。至于 Upvalue 的具体内存存储形式、Lua 虚拟机的调用栈和 GC 等底层机制,可以在后续学习 Lua 内存管理时再继续展开。
