迭代器( iterator
)是一种可以让我们遍历一个集合中所有元素的代码结构。在 Lua
语言中,通常使用函数表示迭代器:每一次调用函数时,函数会返回集合中的“下一个”元素。一个典型的例子就是 io.read
,每次调用该函数时它都会返回标准输入中的下一行,在没有可以读取的行时返回 nil
。
所有的迭代器都需要在连续的调用之间保存一些状态,这样才能知道当前迭代所处的位置及如何从当前位置步进到下一位置。对于函数 io.read
而言,C语言会将状态保存在流的结构体中。对于我们自己的迭代器而言,闭包则为保存状态提供了一种良好的机制。请注意:一个闭包就是一个可以访问其自身的环境中一个或多个局部变量的函数。这些变量将连续调用过程中的值并将其保存在闭包中,从而使得闭包能够记住迭代所处的位置。当然,要创建一个新的闭包,我们还必须创建非局部变量。因此,一个闭包结构通常涉及两个函数:闭包本身和一个用于创建该闭包及其封装变量的工厂( factory
)。
作为示例,让我们来为列表编写一个简单的迭代器。与 ipairs
不同的是,该迭代器并不是返回每个元素的索引值而是返回元素的值:
function values (t) local i = 0 return function () i = i +1; return t[i] end end
在这个例子中, values
就是工厂。每当调用这个工厂时,它就会创建一个新的闭包(即迭代器本身)。这个闭包将它的状态保存在其外部的变量 t
和 i
中,这两个变量也是由 values
创建的。每次调用这个迭代器时,它就从列表 t
中返回下一个值。在遍历完最后一个元素后,迭代器返回 nil
,表示迭代结束。
我们可以在一个 while
循环中使用这个迭代器:
t = {10, 20, 30} iter = values(t) -- 创建迭代器 while true do local element = iter() -- 调用迭代器 if element == nil then break end print(element) end
不过,使用泛型for更简单。毕竟,泛型for正是为了这种迭代而设计的:
t = {10, 20, 30} for element in values(t) do print(element) end
泛型for为一次迭代循环做了所有的记录工作:它在内部保存了迭代函数,因此不需要变量 iter
;它在每次做新的迭代时都会再次调用迭代器,并在迭代器返回 nil
时结束循环。
来看一个更高级的示例,这个示例展示了一个迭代器,它可以遍历来自标准输入的所有单词:
function allwords() local line = io.read() -- 当前行 local pos = 1 -- 当前行的当前位置 return function() -- 迭代函数 while line do -- 当还有行时循环 local w, e = string.match(line, "(%w+)()", pos) if w then -- 发现一个单词? pos = e -- 下一个位置位于该单词后 return w -- 返回该单词 else line = io.read() -- 没找到单词,尝试下一行 pos = 1 -- 从第一个位置重新开始 end end return nil -- 没有行了,迭代结束 end end
为了完成这样的遍历,我们需要保存两个值:当前行的内容(遍历 line
)及当前行的当前位置(遍历 pos
)。有了这些数据,我们就可以不断产生下一个单词。这个迭代函数的主要部分是调用函数 string.match
,以当前位置作为起始在当前行中搜索一个单词。函数 string.match
使用模式 '%w+'
来匹配一个单词,也就是匹配一个或多个字母/数字字符。如果函数 string.match
找到了一个单词,它就捕获并返回这个单词及该单词之后的第一个字符的位置(一个空匹配),迭代函数则更新当前位置并返回该单词,否则,迭代函数读取新的一行,然后重复上述搜索过程。在所有的行都被读取完后,迭代函数返回 nil
以表示迭代结束。
尽管迭代器本身有点复杂,但allwords的使用还是很简明易懂的:
for word in allwords() do print(word) end
对于迭代器而言,一种常见的情况就是:编写迭代器可能不太容易,但使用迭代器却十分简单。这也不是一个大问题,因为使用 Lua
语言编程的最终用户一般不会取定义迭代器,而只会使用那些宿主应用已经提供的迭代器。