内容来自《Lua程序设计(第四版)》18.1节 —— 迭代器和闭包。
迭代器(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
,以当前位置作为起始在当前行中搜索一个单词。函数使用模式 %w+
来匹配一个单词(也就是匹配一个或多个字母或数字字符),如果找到了一个单词,就捕获并返回这个单词及该单词之后的第一个字符的位置(一个空匹配),迭代函数则更i性能当前位置并返回该单词;否则,迭代器读取新的一行,然后重复上述搜索过程。在所有的行都被读取完后,迭代函数返回 nil 以表示迭代结束。
尽管迭代器本身比较复杂,但 allwords
的使用还是很简单易懂的:
for word in allwords() do
print(word)
end
对于迭代器而言,一种很常见的情况就是:编写迭代器可能不太容易,但是使用迭代器却十分简单,这也不是大问题,因为使用 Lua 语言编程的最终用户一般不会去定义迭代器,而是使用那些宿主因应用已经提供的迭代器。