我们可以将循环迭代器视为生产者-消费者模式的一种特例:一个迭代器会产生有循环体消费的内容。因此,用协程来实现迭代器看上去就很合适。的确,协程为实现这类任务提供了一种强大的工具。同时,协程最关键的特性是能够颠倒调用者与被调用者之间的关系。有了这种特性,我们在编写迭代器时就无需担心如何保存连续调用之间的状态了。
为了说明这类用途,让我们来编写一个遍历指定数组所有排列的迭代器。要直接编写这种迭代器并不容易,但如果要编写一个递归函数来产生所有排列则不是很难。思路很简单,只要依次将每个数组元素放到最后一个位置,然后递归生成其余元素的所有排列即可。
function permgen(a, n) n = n or #a -- n的默认大小是a if n <= 1 then -- 只有一种组合 printResult(a) else for i = 1, n do a[n], a[i] = a[i], a[n] -- 把第i个元素当做最后一个 permgen(a, n - 1) -- 生成其余元素的所有排列 a[n], a[i] = a[i], a[n] -- 恢复第i个元素 end end end
还需要定义一个合适的函数 printResult
来输出结果,并使用恰当的参数调用 permgen
:
function printResult(a) for i = 1, #a do io.write("\n") end end permgen({1, 2, 3, 4}) --> 2 3 4 1 --> 3 2 4 1 --> 3 4 2 1 …… --> 2 1 3 4 --> 1 2 3 4
当有了生成器后,将其转换为迭代器就很容易了。首先,我们把 printResult
改为 yield
:
function permgen (a, n) n = n or #a if n <= 1 then coroutine.yield(a) else -- 同前
然后,我们定义一个将生成器放入协程运行并创建迭代函数的工厂。迭代器只是简单地唤醒协程,让其产生下一个排列:
function permutation (a) local co = coroutine.create(function () permgen(a) end) return function() local code, res = coroutine.resume(co) return res end end
有了上面的这些,在 for
循环中遍历一个数组的所有排列就非常简单了:
for p in permutations {"a", "b", "c"} do printResult(p) end --> b c a --> c b a --> c a b --> a c b --> b a c --> a b c
函数 permutations
使用了 Lua
语言中一种常见的模式,就是将唤醒对应协程的调用包装在一个函数中。由于这种模式比较常见,所以 Lua
语言专门提供了一个特殊的函数 coroutine.wrap
来完成这个功能。与函数 create
类似,函数 wrap
也用来创建一个新的协程。但不同的是,函数 wrap
返回的不是协程本身而是一个函数,当这个函数被调用时会唤醒协程。与原始的函数 resume
不同,该函数的第一个返回值不是错误代码,当遇到错误时该函数会抛出异常。我们可以使用函数 wrap
改写 permutations
:
function permutations (a) return coroutine.wrap(function() permgen(a) end) end
通常,函数 coroutine.wrap
比函数 coroutine.create
更易于使用。它为我们提供了对于操作协程而言所需的功能,即一个唤醒协程的函数。不过,该函数缺乏灵活性,我们无法检查通过函数 wrap
所创建的协程的状态,也无法检查运行时的异常。