一等迭代器
Nim 中有两种迭代器: inline (内联)和 closure (闭包)迭代器。 inline iterator “内联迭代器” 总是被编译器内联优化, 这种抽象也就不会带来任何额外开销(零成本抽象),但代码体积可能大大增加。
请警惕: 在使用内联迭代器时,循环体会被内联进循环中所有的 yield 语句里,所以理想情况是合理地重构迭代器代码使它只包含一条 yield 语句,以免代码体积膨胀。
内联迭代器是二等公民;它们只能作为参数传递给其他内联代码工具,如模板、宏和其他内联迭代器。
相反,closure iterator “闭包迭代器” 可以更自由地传递:
iterator count0(): int {.closure.} =
yield 0
iterator count2(): int {.closure.} =
var x = 1
yield x
inc x
yield x
proc invoke(iter: iterator(): int {.closure.}) =
for x in iter(): echo x
invoke(count0)
invoke(count2)
闭包迭代器和内联迭代器都有一些限制:
- 目前,闭包迭代器不能在编译期执行。
- 闭包迭代器可使用 return 语句结束循环,但内联迭代器(虽然基本没什么用)不允许使用。
- 内联迭代器不能递归。
- 内联迭代器与闭包迭代器都没有特殊的 result 变量。
- JS 后端不支持闭包迭代器。
如果既不用 {.closure.} 也不用 {.inline.} 显式标记迭代器,则默认为内联迭代器。但是将来的版本可能会改动。
iterator 类型总是约定隐式使用 closure 调用规范;下面的例子展示了如何使用迭代器实现一个 collaborative tasking “协作任务”系统:
# simple tasking:
type
Task = iterator (ticker: int)
iterator a1(ticker: int) {.closure.} =
echo "a1: A"
yield
echo "a1: B"
yield
echo "a1: C"
yield
echo "a1: D"
iterator a2(ticker: int) {.closure.} =
echo "a2: A"
yield
echo "a2: B"
yield
echo "a2: C"
proc runTasks(t: varargs[Task]) =
var ticker = 0
while true:
let x = t[ticker mod t.len]
if finished(x): break
x(ticker)
inc ticker
runTasks(a1, a2)
可以使用内置的 system.finished 判断迭代器是否结束;如果迭代器已经结束,再次调用也不会抛出异常。
请注意 system.finished 容易用错,因为它只在迭代器最后一次循环完成后的下一次迭代才返回 true:
iterator mycount(a, b: int): int {.closure.} =
var x = a
while x <= b:
yield x
inc x
var c = mycount # 实例化迭代器
while not finished(c):
echo c(1, 3)
# 输出
1
2
3
0
所以这段代码应该这么写:
var c = mycount # 实现化迭代器
while true:
let value = c(1, 3)
if finished(c): break # 丢弃这次的返回值!
echo value
为了便于理解,可以这样认为,迭代器实际上返回了键值对 (value, done),而 finished 的作用就是访问隐藏的 done 字段。
闭包迭代器是 可恢复函数 ,因此每次调用必须提供参数。如果需要绕过这个限制,可以通过工厂过程构造闭包迭代器,并在构造的时候捕获参数:
proc mycount(a, b: int): iterator (): int =
result = iterator (): int =
var x = a
while x <= b:
yield x
inc x
let foo = mycount(1, 4)
for f in foo():
echo f
借助 for 循环宏可以把这个函数调用变得像是在使用内联迭代器:
import std/macros
macro toItr(x: ForLoopStmt): untyped =
let expr = x[0]
let call = x[1][1] # 把 foo 拿从 toItr(foo) 里出来
let body = x[2]
result = quote do:
block:
let itr = `call`
for `expr` in itr():
`body`
for f in toItr(mycount(1, 4)): # 使用上文的 `proc mycount`
echo f
因为闭包迭代器需要以完整的函数调用机制作为支撑,所以代价比调用内联迭代器更高。 像这样在使用闭包迭代器的地方用宏装饰一下,或许是一种有益的提醒。
工厂过程 proc 同普通的过程一样也可以递归。利用上面的宏可让这种过程的递归看起来像是递归迭代器在递归。比如:
proc recCountDown(n: int): iterator(): int =
result = iterator(): int =
if n > 0:
yield n
for e in toItr(recCountDown(n - 1)):
yield e
for i in toItr(recCountDown(6)): # 输出: 6 5 4 3 2 1
echo i
另请参阅iterable将迭代器传递给模板和宏。