一等迭代器

Nim 中有两种迭代器: inline (内联)和 closure (闭包)迭代器。 inline iterator “内联迭代器” 总是被编译器内联优化, 这种抽象也就不会带来任何额外开销(零成本抽象),但代码体积可能大大增加。

请警惕: 在使用内联迭代器时,循环体会被内联进循环中所有的 yield 语句里,所以理想情况是合理地重构迭代器代码使它只包含一条 yield 语句,以免代码体积膨胀。

内联迭代器是二等公民;它们只能作为参数传递给其他内联代码工具,如模板、宏和其他内联迭代器。

相反,closure iterator “闭包迭代器” 可以更自由地传递:

  1. iterator count0(): int {.closure.} =
  2. yield 0
  3. iterator count2(): int {.closure.} =
  4. var x = 1
  5. yield x
  6. inc x
  7. yield x
  8. proc invoke(iter: iterator(): int {.closure.}) =
  9. for x in iter(): echo x
  10. invoke(count0)
  11. invoke(count2)

闭包迭代器和内联迭代器都有一些限制:

  1. 目前,闭包迭代器不能在编译期执行。
  2. 闭包迭代器可使用 return 语句结束循环,但内联迭代器(虽然基本没什么用)不允许使用。
  3. 内联迭代器不能递归。
  4. 内联迭代器与闭包迭代器都没有特殊的 result 变量。
  5. JS 后端不支持闭包迭代器。

如果既不用 {.closure.} 也不用 {.inline.} 显式标记迭代器,则默认为内联迭代器。但是将来的版本可能会改动。

iterator 类型总是约定隐式使用 closure 调用规范;下面的例子展示了如何使用迭代器实现一个 collaborative tasking “协作任务”系统:

  1. # simple tasking:
  2. type
  3. Task = iterator (ticker: int)
  4. iterator a1(ticker: int) {.closure.} =
  5. echo "a1: A"
  6. yield
  7. echo "a1: B"
  8. yield
  9. echo "a1: C"
  10. yield
  11. echo "a1: D"
  12. iterator a2(ticker: int) {.closure.} =
  13. echo "a2: A"
  14. yield
  15. echo "a2: B"
  16. yield
  17. echo "a2: C"
  18. proc runTasks(t: varargs[Task]) =
  19. var ticker = 0
  20. while true:
  21. let x = t[ticker mod t.len]
  22. if finished(x): break
  23. x(ticker)
  24. inc ticker
  25. runTasks(a1, a2)

可以使用内置的 system.finished 判断迭代器是否结束;如果迭代器已经结束,再次调用也不会抛出异常。

请注意 system.finished 容易用错,因为它只在迭代器最后一次循环完成后的下一次迭代才返回 true:

  1. iterator mycount(a, b: int): int {.closure.} =
  2. var x = a
  3. while x <= b:
  4. yield x
  5. inc x
  6. var c = mycount # 实例化迭代器
  7. while not finished(c):
  8. echo c(1, 3)
  9. # 输出
  10. 1
  11. 2
  12. 3
  13. 0

所以这段代码应该这么写:

  1. var c = mycount # 实现化迭代器
  2. while true:
  3. let value = c(1, 3)
  4. if finished(c): break # 丢弃这次的返回值!
  5. echo value

为了便于理解,可以这样认为,迭代器实际上返回了键值对 (value, done),而 finished 的作用就是访问隐藏的 done 字段。

闭包迭代器是 可恢复函数 ,因此每次调用必须提供参数。如果需要绕过这个限制,可以通过工厂过程构造闭包迭代器,并在构造的时候捕获参数:

  1. proc mycount(a, b: int): iterator (): int =
  2. result = iterator (): int =
  3. var x = a
  4. while x <= b:
  5. yield x
  6. inc x
  7. let foo = mycount(1, 4)
  8. for f in foo():
  9. echo f

借助 for 循环宏可以把这个函数调用变得像是在使用内联迭代器:

  1. import std/macros
  2. macro toItr(x: ForLoopStmt): untyped =
  3. let expr = x[0]
  4. let call = x[1][1] # 把 foo 拿从 toItr(foo) 里出来
  5. let body = x[2]
  6. result = quote do:
  7. block:
  8. let itr = `call`
  9. for `expr` in itr():
  10. `body`
  11. for f in toItr(mycount(1, 4)): # 使用上文的 `proc mycount`
  12. echo f

因为闭包迭代器需要以完整的函数调用机制作为支撑,所以代价比调用内联迭代器更高。 像这样在使用闭包迭代器的地方用宏装饰一下,或许是一种有益的提醒。

工厂过程 proc 同普通的过程一样也可以递归。利用上面的宏可让这种过程的递归看起来像是递归迭代器在递归。比如:

  1. proc recCountDown(n: int): iterator(): int =
  2. result = iterator(): int =
  3. if n > 0:
  4. yield n
  5. for e in toItr(recCountDown(n - 1)):
  6. yield e
  7. for i in toItr(recCountDown(6)): # 输出: 6 5 4 3 2 1
  8. echo i

另请参阅iterable将迭代器传递给模板和宏。