Lua 迭代器(实战指南)

什么是 Lua 迭代器?从基础概念说起

在编程的世界里,遍历数据是再常见不过的操作。无论是遍历一个数组、列表,还是处理一个复杂的对象结构,我们都需要一种方式“逐个访问”其中的元素。在 Lua 中,这种机制的核心就是 Lua 迭代器

你可以把迭代器想象成一位“快递员”——他手握一份订单清单,每次从仓库里取出一件商品,送到你手上。你不需要知道仓库内部的结构,也不需要关心商品是如何存放的,只需要告诉快递员:“下一个”,他就会把下一件商品递给你。这种“按需取货”的方式,正是迭代器的本质。

Lua 提供了两种迭代器:简单的 for 循环迭代器自定义的迭代函数。前者适合处理数组、表等结构,后者则赋予你更大的灵活性,可以处理任何你想遍历的数据类型。

比如,最常见的 for-in 循环:

for i, v in ipairs({ "apple", "banana", "orange" }) do
    print(i, v)
end

这段代码会依次输出:

1    apple
2    banana
3    orange

这里 ipairs 就是一个内置的迭代器函数,它返回一个迭代器函数、状态值和初始值。整个过程由 Lua 内部自动管理,你只需要关注如何处理每个元素即可。

内置迭代器:ipairs 与 pairs 的区别

Lua 提供了两个常用的内置迭代器:ipairspairs,它们虽然都用于遍历表(table),但行为却大不相同。

ipairs 专为连续的数值索引数组设计。它只遍历从 1 开始、连续递增的键值对,一旦遇到 nil 或非整数索引,就会停止。比如:

local fruits = { "apple", "banana", nil, "orange" }

for i, v in ipairs(fruits) do
    print(i, v)
end

输出结果是:

1    apple
2    banana

注意,nil 位置之后的 "orange" 被跳过了。这是 ipairs 的设计逻辑:它认为数组在遇到 nil 时已经“结束”。

pairs 则不同,它遍历表中所有键值对,无论键是否为数字,是否连续。例如:

local person = {
    name = "Alice",
    age = 25,
    city = "Beijing",
    [1] = "first",
    [3] = "third"
}

for k, v in pairs(person) do
    print(k, v)
end

输出可能是:

name    Alice
age     25
city    Beijing
1       first
3       third

注意,键的顺序是不确定的。pairs 不保证顺序,这是它的特性,而非 bug。

特性 ipairs pairs
遍历范围 从 1 开始的连续整数键 所有键值对
是否跳过 nil
键的顺序 严格递增 不保证
适用场景 数组类数据 任意表结构

选择使用哪个迭代器,取决于你的数据结构和需求。如果你处理的是一个真正的“数组”,用 ipairs 更安全;如果表中包含非数字键或稀疏结构,pairs 是更合适的选择。

自定义迭代器:从零构建你的遍历逻辑

虽然内置迭代器够用,但当你需要遍历一个自定义的数据结构,比如一个链表、树,或者一个按规则生成的序列时,就必须自己编写迭代器。

在 Lua 中,自定义迭代器的本质是一个三元组:一个迭代函数、一个状态值和一个初始值。Lua 的 for-in 循环会自动调用这个三元组,直到迭代函数返回 nil

我们来实现一个简单的“递增计数器”迭代器:

-- 自定义迭代器函数:每次返回当前值并加1
local function counter_iter(state, value)
    -- state 是状态,value 是当前值
    -- 当 value 超过 5 时停止
    if value > 5 then
        return nil  -- 停止迭代
    end
    -- 返回下一个值和新的状态
    return value + 1, value + 1
end

-- 使用这个迭代器
for i in counter_iter, 1, 1 do
    print("当前值:", i)
end

输出结果:

当前值: 1
当前值: 2
当前值: 3
当前值: 4
当前值: 5

这个例子中:

  • counter_iter 是迭代函数
  • 1 是初始状态(初始值)
  • 1 是初始值(这里状态和初始值相同)

每次循环调用 counter_iter(1, 1),返回 2, 2,然后下一次传入 2, 2,依此类推。直到返回 nil

这个模式非常强大,你可以用它来遍历任何结构。比如一个简单的链表:

-- 模拟链表节点
local list = {
    value = 10,
    next = {
        value = 20,
        next = {
            value = 30,
            next = nil
        }
    }
}

-- 遍历链表的迭代器
local function list_iter(state, value)
    if state == nil then
        return nil  -- 到头了
    end
    local current = state
    local next_node = current.next
    return next_node, next_node  -- 返回下一个节点和状态
end

-- 使用迭代器遍历链表
for node in list_iter, list do
    print("节点值:", node.value)
end

输出:

节点值: 20
节点值: 30

这里 statelist 开始,每次返回 next 节点,直到 nextnil,迭代结束。

迭代器的闭包应用:状态管理的艺术

在实际开发中,我们常需要在迭代过程中维护一些状态,比如计数器、过滤条件、或当前索引。Lua 的闭包特性让这变得异常优雅。

考虑一个需求:遍历一个表,但只输出偶数键对应的值。

local data = {
    [1] = "odd",
    [2] = "even",
    [3] = "odd",
    [4] = "even",
    [5] = "odd"
}

-- 创建一个只返回偶数键的迭代器
local function even_key_iter(t)
    local i = 0  -- 闭包中的状态变量

    -- 返回一个迭代函数
    return function()
        i = i + 1
        -- 检查是否为偶数键
        while i <= #t do
            if i % 2 == 0 then
                return i, t[i]
            end
            i = i + 1
        end
        return nil  -- 没有更多偶数键
    end
end

-- 使用这个迭代器
for k, v in even_key_iter(data) do
    print("偶数键:", k, "值:", v)
end

输出:

偶数键: 2 值: even
偶数键: 4 值: even

关键点在于:

  • even_key_iter 返回一个函数(闭包)
  • 闭包内部的 i 变量在多次调用中持续存在
  • 每次调用迭代函数,i 会递增,直到找到下一个偶数键

这种写法避免了在外部维护状态,代码更简洁,逻辑更清晰。

实战案例:遍历目录文件并过滤类型

我们来做一个更贴近实际的案例:模拟遍历一个目录中的文件,只输出 .lua 文件。

虽然 Lua 本身不直接支持文件系统,但我们可以用一个表来模拟目录结构。

-- 模拟目录结构
local directory = {
    "main.lua",
    "utils.lua",
    "config.txt",
    "data.json",
    "test.lua"
}

-- 创建一个迭代器,只返回以 .lua 结尾的文件名
local function lua_files_iter(files)
    local i = 0  -- 状态:当前索引

    return function()
        i = i + 1
        -- 遍历到结尾
        if i > #files then
            return nil
        end
        local filename = files[i]
        -- 检查是否以 .lua 结尾
        if filename:match("%.lua$") then
            return filename
        end
        -- 否则跳过,继续下一个
        return nil
    end
end

-- 使用迭代器遍历
print("Lua 文件列表:")
for file in lua_files_iter(directory) do
    print("  ", file)
end

输出:

Lua 文件列表:
   main.lua
   utils.lua
   test.lua

这个例子展示了迭代器在过滤数据时的强大能力。你不需要把所有文件都加载到内存,也不需要先生成一个新表,而是“边遍历,边过滤”。

总结与进阶建议

Lua 迭代器是 Lua 语言中极为优雅的设计之一。它把“遍历”这个动作从数据结构中剥离出来,使得代码更灵活、更可复用。

通过本文,你已经掌握了:

  • 内置迭代器 ipairspairs 的区别与使用场景
  • 如何编写自定义迭代器,实现对任意数据结构的遍历
  • 利用闭包管理状态,写出更简洁的迭代逻辑
  • 在实际项目中如何用迭代器做数据过滤和流式处理

当你在项目中频繁遍历数据时,不妨先问问自己:是否可以用一个迭代器来简化逻辑?它不仅能提升代码可读性,还能降低出错概率。

记住,迭代器的本质是“按需生成下一个元素”,而不是“一次性加载全部数据”。这种惰性求值的思维,正是现代编程中高效处理大数据的核心理念。

如果你正在学习 Lua,或者在开发游戏、脚本系统、配置引擎,深入理解 Lua 迭代器,会让你的代码更接近“Lua 风格”。