(译)Dart语言异步支持:第二阶段

Async*, sync*, 以及剩余部分

作者 Gilad Bracha,作于2015年三月。
在之前的文章中,我们谈及了异步方法和等待表达式。这些功能是Dart中支持异步编程和生成器的完整计划的一部分。

生成器( Generators
)

Dart 1.9引入了生成器功能。这些方法能惰性地计算出一系列的结果。有两种生成器 – 同步的和异步的。同步生成器按需生成值 – 消费者从生成器中拉取值。异步生成器按照自己的速度生成值,并将它们推送到消费者可以找到的位置。

为什么在语言中支持生成器

人们可以手动实现生成器,但这可能很棘手,而且肯定是乏味的。
要实现同步生成器,您需要定义自己的可迭代类。你可以继承IterableBase,但你仍然需要声明该类并实现迭代器方法,该方法必须返回一个新的迭代器。要做到这一点,你必须声明自己的迭代器类。您需要实现成员moveNext()和current,它们跟踪并更新迭代器的位置,检测你何时到达底层迭代的末尾(以及它是否为空开始)。这没什么大不了的 – 我们知道你喜欢编程,这是一个很棒的CS101练习。但是,也许,只是也许,你想花时间编程别的东西。同步发生器功能是用于实现这种可迭代的语法糖。
手动编写异步生成器更有趣。您可以编写棘手的样板,而不仅仅是样板文件。例如,当流被暂停或取消时,您必须确保一切正常。
Dart新的内置生成器支持使事情变得更加容易,我们将在下面看到。

同步生成器:sync

使用sync*修饰符标记方法体将该方法标识为同步生成器,并减轻程序员手动定义迭代所涉及的大部分样板。
假设我们想要产生前n个自然数。使用同步生成器很容易做到这一点。

Iterable naturalsTo(n) sync* {
  int k = 0;
  while (k < n) yield k++;
}

调用时,naturalsTo立即返回一个iterable(很像标记为async的函数,立即返回一个 future
),从中可以提取迭代器。在迭代器上调用moveNext之前,函数体不会开始运行。它将一直运行,直到它第一次达到yield语句。 yield语句包含一个表达式,它会对其进行求值。然后,函数挂起,moveNext将true返回给它的调用者。
该函数将在下次调用moveNext时继续执行。当循环结束时,该方法隐式执行return,导致它终止。此时,moveNext将false返回给其调用者。
使用普通迭代器,这可能会非常繁琐,因为必须定义专门的迭代器和可迭代类并实现完整的Iterable API。

魔鬼的细节

您可以将同步与*分开;他们是不同的符号。如果你有使用同步作为标识符的现有代码,则可以继续这样做。单词sync不是真正的保留字。类似的解释适用于 async
, await
, 以及 yield
。它们仅被视为异步或生成器函数内的保留字(即标记为async,sync*或异步async*的那些)。

请记住,遵守严格的兼容性需要付出代价。假设你忘记在函数上使用诸如sync *之类的修饰符,并在其中使用 yield
语句。解析器可能会非常困惑,并给出相当令人困惑的错误消息。

异步生成器:async*

为了异步生成序列,我们使用了流。可以使用Stream和联合类手动实现流。异步生成器函数是用于实现此类流的糖。使用async*修饰符标记函数体将该函数标识为异步生成器。
让我们尝试再次生成自然数,这次是异步的。

Stream asynchronousNaturalsTo(n) async* {
  int k = 0;
  while (k < n) yield k++;
}

调用此函数会立即返回一个流 – 正如调用sync*函数会立即返回一个迭代器,并且调用异步函数会立即返回一个 future
(也许你可以在这里看到一个模式)。
一旦你监听到流,函数体的执行就开始了。 yield语句执行时,会将其表达式的计算结果添加到流中。它不一定是暂停的(尽管在当前的实现中它确实如此)。
在任何情况下,所有监听着流的方法都会在某个节点有新值的时候被调用。然而,主动权不在消费者手中;流将其值乐此不彼推送到监听器函数。

难懂的条文

作为变体,请考虑

Stream get naturals async* {
  int k = 0; while (true) { yield await k++; }
}

这个例子提出了一个有趣的问题:因为代码在一个紧密的无限循环中运行而yield不会挂起,什么时候任何监听器都要运行来查看结果?我们可能要求产量总是暂停,但这可能会损害性能。唯一的要求是函数最终会暂停,以便其他一些代码可以运行并从流中提取值。

与async*函数关联的流可能会暂停或取消。如果async*函数执行yield并且其流已被取消,则控制转移到最近的封闭finally子句。如果流已暂停,则执行在yield之前暂停,直到恢复流。有关所有血腥细节,请参阅 Dart语言规范

await-for

正如每个Dart程序员都知道的那样,for-in循环可以与iterables一起使用。类似地,await-for循环旨在与流良好匹配。
给定一个流,可以循环其值:

await for (int i in naturals) { print(‘event loop $i’); }

每次将元素添加到流中时,都会运行循环体。在每次迭代之后,包含循环的函数将暂停,直到下一个元素可用或流完成为止。就像await表达式一样,await-for循环只能出现在异步函数中。

yeild*

虽然yield的使用很有吸引力,但你可能会遇到问题。如果你正在编写递归函数,则可以获得二次方行为。考虑以下功能,旨在从n向后计数。

Iterable naturalsDownFrom(n) sync* {
  if (n > 0) {
     yield n;
     for (int i in naturalsDownFrom(n-1)) { yield i; }
  }
}

上面的代码在功能上是正确的,但是以二次方式运行。请注意, yeild i
;对于序列中的第n个元素执行n-1次:在每个递归级别执行一次;第一个元素3仅由产量n产生;声明;第二个元素2由 yield n
产生一次;并且一次产量i;第三个元素是1,由yield n产生一次;产量两倍;总共我们有n(n-1)次执行 yield i
;,即O(n2)。
yield*(发音为yield-each)语句旨在解决此问题。 yield *之后的表达式必须表示另一个(子)序列。 yield*的作用是将子序列的所有元素插入到当前正在构造的序列中,就好像我们每个元素都有一个单独的yield。我们可以使用yield-each重写我们的代码,如下所示:

Iterable naturalsDownFrom(n) sync* {
  if ( n > 0) {
    yield n;
    yield* naturalsDownFrom(n-1);
 }
}

后一版本以线性时间运行。

难懂的条文

在sync*函数中,子序列必须是可迭代的;在async*方法中,子序列必须是流。如果不是,则会有运行时错误。当然,在这种情况下,您也会收到静态警告。
子序列可以为空。在这种情况下,yield *会跳过它而不会挂起。