Dart中yield和yield*的区别

58

我想知道这两者之间的区别。在javascript方面,我找到了这个Stack Overflow的帖子,Generator函数中的被委派的yield(yield star,yield *)

据我所知,yield*委托给另一个生成器,在另一个生成器停止产生值后,它会继续生成自己的值。

对dart方面的解释和示例将会很有帮助。

5个回答

73

yield

它用于从生成器中发出值,可以是异步或同步的。

示例:

Stream<int> getStream(int n) async* {
  for (var i = 1; i <= n; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

void main() {
  getStream(3).forEach(print);
}

输出:

1
2
3

yield*

它将调用委托给另一个生成器,并在该生成器停止产生值后,恢复生成自己的值。

示例:

Stream<int> getStream(int n) async* {
  if (n > 0) {  
    await Future.delayed(Duration(seconds: 1));
    yield n; 
    yield* getStream(n - 1); 
  }
}

void main() {
  getStream(3).forEach(print);
}

输出:

3
2 
1 

如果我在生成器内使用 yield numbersDownFrom(n - 1) 而不是 yield* numbersDownFrom(n - 1);,会发生什么? - sonic
@sonic yield*用于将调用委托给生成器,因为yield numbersDownFrom(n-1)不会有任何意义,因为yield只会产生您在Stream中想要返回的类型。 - CopsOnRoad
我需要自己测试一下。我只是好奇它会被标记为错误还是会做出一些意外的事情。感谢你的回答。 - sonic
答案开头有一个错误:句子“它用于从生成器中异步或同步地发出值。”应该是“它用于从生成器中异步或同步地发出值。”请参见https://jelenaaa.medium.com/what-are-sync-async-yield-and-yield-in-dart-defe57d06381。 - Jean-Pierre Schnyder

57

简短回答

  • yield 在可迭代对象或流中提供单个值。
  • yield* 从另一个可迭代对象或流中提供多个值。

可迭代对象(同步)示例

看一下以下两个可迭代对象是如何交互的:

void main() {
  final myIterable = getValues();
  for (int value in myIterable) {
    print(value);
  }
}

Iterable<int> getValues() sync* {
  yield 42;
  yield* getThree();
  yield* [6, 7, 8];
  yield 24;
}

Iterable<int> getThree() sync* {
  yield 1;
  yield 2;
  yield 3;
}

这是发生的事情:
- `sync*` 创建一个可迭代对象,使其成为一个同步生成器函数。 - `yield 42` 为 `getValues` 生成器函数提供一个单个值(42)。 - `yield*` 通过从 `getThree` 生成器函数请求可迭代对象的所有值,为 `getValues` 提供多个值。 - 由于列表也是可迭代的,`yield* [6, 7, 8]` 为 `getValues` 生成器函数产生列表中的每个值。 - 最后,`yield 24` 为 `getValues` 提供另一个单个值。此值仅在之前对其他可迭代对象的 `yield*` 调用完成后提供。
运行上述代码,您将看到以下结果:
42
1
2
3
6
7
8
24

异步流示例

现在让我们来看看流版本:

Future<void> main() async {
  final myStream = getValues();
  await for (int value in myStream) {
    print(value);
  }
}

Stream<int> getValues() async* {
  yield 42;
  yield* getThree();
  yield* Stream.fromIterable([6, 7, 8]);
  yield 24;
}

Stream<int> getThree() async* {
  yield 1;
  yield 2;
  yield 3;
}

注意:

  • async*创建一个流,使其成为一个异步生成器函数
  • 与以前一样,yield提供一个单一的值。
  • yield*从另一个流中提供多个值。
  • yield*在继续之前等待被调用的流完成。

运行它,你将再次看到以下输出:

42
1
2
3
6
7
8
24

这个答案是我原来答案的完全重写(请参阅编辑历史),在那里我对yield*的含义产生了严重的误解,即使在观看了Generator Functions - Flutter in Focus视频之后。感谢@Hackmodford在评论中纠正了我的错误。

1
@Hackmodford,非常感谢您的帮助。我现在明白了。我已经重新修改了我的答案,如果您有时间,请看一下并指出我是否还有任何错误。 - Suragch
1
是的!这是我对它如何工作的理解。我已经更新了我的投票。 - Hackmodford
(...继续...)-- 或许可以从展示awaitasync*情况下的使用方式开始,但在sync*情况下却不能使用。因此我推测yield*只能在sync*生成器中使用,如果周围的函数是一个sync*生成器的话,而yield*可以在async*sync*生成器中使用,在async*生成器内部。另外可以指出的一件事是,*只是表示生成器,没有其他含义,而sync*需要明确指定的唯一原因是在函数后面只加上*不是一个好的语法。 - undefined
@Suragch 我在我的回答中写了所有这些,请查看一下。 - undefined
1
@LukeHutchison,很高兴你能找到答案。我在这里并没有关注可迭代对象和流的区别,所以很高兴有你的答案来回答其他有同样问题的人。+1 - undefined
显示剩余4条评论

5

我创建了一个Dart Pad链接,以帮助人们进行实验:

Yield*用于一次返回整个可迭代对象一个值,而无需使用循环。

这两个函数完全相同,基于开始和结束值生成一个可迭代对象。

Iterable<int> getRangeIteration(int start, int finish) sync* {
  for(int i = start; i<= finish; i++){
  yield i;
  }
}

Iterable<int> getRangeRecursive(int start, int finish) sync* {
  if (start <= finish) {
    yield start;
    yield* getRangeRecursive(start + 1, finish);
  }
}

在第一种实现中(yield i)意味着类型Iterable,与函数的返回类型相匹配。
现在,如果在第二种实现中,而不是yield i,
 yield* getRangeRecursive(start + 1, finish);

如果我们这样做

yield getRangeRecursive(start + 1, finish);

我们将会得到一个编译器错误:

The type 'Iterable<Iterable<int>>' implied by the 'yield' expression must be assignable to 'Iterable<int>'.

正如您所看到的,yield将可迭代对象包装在另一个Iterable<>中,使其类型为Iterable<Iterable>。这与函数的返回类型不匹配。

如果我们必须在不使用yield*的情况下进行递归,我们需要这样做:

Iterable<int> getRangeRecursiveWithOutYieldStar(int start, int finish) sync* {
  if (start <= finish) {
    yield start;
    for (final val in getRangeRecursiveWithOutYieldStar(start + 1, finish)){
    yield val;
    }
  }
}

这样做会变得混乱且效率低下。

因此,在我看来,yield* 会展开另一个生成器函数。

一些好的资源: Flutter in Focus视频 - 生成器函数

中等大小的文章: Dart中的sync*、async*、yield和yield*是什么?


1

我花了一些时间才弄明白,但这是我目前的理解:

*的意义

*只是表示“生成器”。所以:

  • sync*是一个同步生成器函数(一个可以yield值但不能await异步函数调用的函数)。sync*函数的类型是Iterable<T>
  • async*是一个异步生成器函数(一个可以yield值的函数,包括在await另一个async函数的函数调用结果之后)。async*函数的类型是Stream<T>,在概念上有点像“异步可迭代对象”,尽管StreamIterable有不同的API,如下所述。
一个生成器函数(可以使用yield关键字产生值的函数)在每个值被yield后,其执行状态会被冻结,直到调用者请求下一个值之前,生成器函数不会执行任何其他代码。这个过程会一直持续,直到生成器函数返回,此时调用者会被告知没有更多的值可用。

forIterablesync*)一起使用,await forStreamasync*)一起使用

假设有以下函数:

  • Iterable<T> syncIterable() sync* {...}
  • Stream<T> asyncStream() async* {...}

如果你想遍历这些生成器函数产生的结果:

  • 对于sync*生成器函数,你需要使用for (var x in syncIterable)
  • 对于async*生成器函数,你需要使用await for (var y in asyncStream)

如果你尝试使用错误类型的生成器,会出现错误:

  • 如果你尝试使用for (var x in asyncStream),你会得到错误:在'for'循环中使用的类型Stream<T>必须实现'Iterable'。这样做的目的是禁止同步代码调用异步代码。同步代码无法跨越异步间隙。
  • 如果你尝试使用await for (var x in syncIterable),你会得到错误:在'for'循环中使用的类型Iterable<T>必须实现'Stream'。异步代码总是可以调用同步代码,但这样做的目的是强调同步生成器在概念上不会将产生值的代码包装在Future<T>中(因此这是一个类型错误)。

IterableStream之间的区别

除了使用forawait for之外,处理IterableStream还有其他方法。例如,对于Iterable,您可以调用syncIterable.iterator来获取一个Iterator,然后通过调用currentmoveNext()来逐个处理值,而不是使用for循环。
然而,Stream没有包含.iterator属性,因为Iterator API是同步的,而Stream是异步的。提供.iterator将允许同步代码调用异步代码而无需await。简单来说,无法将异步API(在这里是Stream)包装在同步包装器(在这里是Iterator)中。再次强调,同步代码无法跨越异步间隙。

yield*的含义

现在考虑一下`yield*`。再次强调,`*`只是表示“生成器”。在这种情况下,这个语法表示“在继续之前,按顺序生成以下生成器中的所有值”。这是一种将操作推迟给另一个生成器的方式。
- 如果在`sync*`函数内部使用`yield*`,它实现的是`for (var x in syncIterator) { yield x; }`。 - 如果在`async*`函数内部使用`yield*`,它实现的是`await for (var y in asyncStream) { yield y; }`。
当然,对于同步和异步情况,它们的工作方式是不同的,但在这种情况下,语法完全相同。在同步和异步上下文中使用`yield*`时,没有像`for`和`await for`之间那样的语法区别。`yield*`语法的语义是依赖于上下文的!(这让我感到惊讶,我本以为在异步情况下应该是`await yield*`,以保持与`await for`的一致性,但这不是Dart语言设计者的选择。)

如果你尝试在错误类型的生成器中使用yield*,你会得到一个错误:

  • 如果你尝试在sync*函数内部调用yield* asyncStream,你会得到一个错误:由'yield*'表达式暗示的类型'Stream<T>'必须可分配给'Iterable<T>'
  • 如果你尝试在async*函数内部调用yield* syncIterable,你会得到一个错误:由'yield*'表达式暗示的类型'Iterable<T>'必须可分配给'Stream<T>'

-1
使用下面的代码来理解为什么在编写递归时不使用yield*会非常低效。
Iterable<int> getRangeYield(int start, int end) sync* {
  if (start <= end) {
    yield start;
    for (final int val in getRangeYield(start + 1, end)) {
      yield val;
    }
  }
}

Iterable<int> getRangeYieldAnalysed(int start, int end) sync* {
  if (start <= end) {
    print('before start $start');
    yield start * 10;
    print('after start $start');
    for (final int val in getRangeYieldAnalysed(start + 1, end)) {
      print('before val $val');
      yield val * 100;
      print('after val $val');
    }
  }
}

Iterable<int> getRangeYieldStar(int start, int end) sync* {
  // same output as getRangeYield()
  if (start < end) {
    yield* getRangeYieldStar(start + 1, end);
  }
  yield start;
}

Iterable<int> getRangeYieldStarAnalysed(int start, int end) sync* {
  // same output as getRangeYield()
  print('generator $start started');
  if (start < end) {
    yield* getRangeYieldStarAnalysed(start + 1, end);
  }
  yield start;
  print('generator $start ended');
}

Iterable<int> getRangeForLoop(int start, int end) sync* {
  // same output as getRangeYield()
  for (int i = start; i <= end; i++) {
    yield i;
  }
}

void main() {
  Iterable<int> it = getRangeYieldStarAnalysed(1, 4);
  print('main range obtained');

  for(int element in it) {
    print('el $element');
  };
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接