【Lua入门系列】从 C++ 迭代器到 Lua 迭代器:一文深入理解 Lua 有状态以及无状态迭代器与泛型for、闭包、ipairs、pairs 和 next

🔥 本文专栏: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,并不是什么原理上做不到的事情。

但是这样会引入新的设计问题:

  1. 每增加一个独立迭代器,就要管理它对应的那一份状态。
  2. 多个函数如何准确地找到各自应使用的状态
  3. 全局变量暴露在全局环境中,其他代码也可能修改它,破坏遍历进度。

所以我们更希望:每个迭代器都有自己的遍历位置,而且这个位置不需要暴露为全局变量。

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 的核心规则:

  1. 先取得迭代器函数、状态值与初始控制变量。
  2. 每次调用迭代器时,都传入 state 和 control。
  3. 将迭代器的返回值依次赋给循环变量。
  4. 第一个返回值还承担特殊职责:作为新的控制变量。
  5. 当第一个返回值为 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 内存管理时再继续展开。


相关推荐
努力实干1 小时前
第二阶段 · 第 7 课:C++ 多线程与线程安全
开发语言·c++·安全
爱聊灵光的维C2 小时前
C语言初阶:关于函数设计的详细讲解和注意事项
c语言·开发语言·青少年编程·蓝桥杯·学习方法
熊猫钓鱼>_>2 小时前
上架审核常见驳回与隐私合规清单:过审前先自查这 8 条【鸿蒙心迹】
开发语言·前端·华为·华为云·harmonyos·openharmony·ai制作
SEO_juper2 小时前
Bash 交叉验证 robots.txt 和 sitemap:两个文件自相矛盾,页面永远收录不了
开发语言·python·node.js·json·bash
geovindu2 小时前
rust: Read-Write Lock Pattern
开发语言·后端·设计模式·rust·读写锁模式·同步型模式
怪我冷i2 小时前
Rust智能指针Rc<T> 使用总结
开发语言·后端·rust
全栈陈序员2 小时前
【Python】基础语法入门(三十三)—— 日志系统详解:用 logging 记录程序运行
java·开发语言·python
ESDWAN2 小时前
SD-WAN 是什么?工作原理、适用场景与选型要点
开发语言·网络·数据库·架构·php
iCxhust3 小时前
C#双时区时钟设计
开发语言·c#