生成器

generator

生成器是Dart中一种特殊的函数,它允许你按需地生成一系列值,而不是一次性返回所有值。这对于处理大量数据、无限序列或需要延迟计算的场景非常有用。

理论: 核心概念与工作原理

在Dart中,生成器是一种返回Iterable(可迭代对象)或Stream(流)的函数,通过使用yield关键字来“暂停”函数的执行,并返回一个值。当请求下一个值时,函数会从上次暂停的地方继续执行。

这与普通的函数不同,普通函数执行完毕后就结束了,并返回一个单一的值(或void)。生成器则可以在多次调用之间保持其状态。

核心原理:

  1. 惰性求值(Lazy Evaluation): 生成器不会在调用时立即计算并返回所有值,而是在每次请求下一个值时才计算并返回,这种方式可以有效节省内存和计算资源。
  2. yield关键字: 它是生成器的核心,用于“产出”一个值。每次yield后,函数会暂停执行,将值发送给调用者。当需要下一个值时,函数会从yield语句的下一行继续执行。
  3. yield*关键字: (Delegating yield)用于将另一个生成器或可迭代对象的内容“委托”给当前的生成器产出。

优势:

  • 内存效率: 特别是在处理大型数据集时,无需一次性将所有数据加载到内存中。
  • 代码简洁: 可以用更直观的方式表示序列,避免手动管理迭代器状态。
  • 处理无限序列: 可以轻松创建无限序列,如斐波那契数列,并在需要时取用。

示例

Dart提供了两种生成器函数:

  • 同步生成器(Synchronous Generators): 返回一个Iterable对象。
  • 异步生成器(Asynchronous Generators): 返回一个Stream对象。

同步生成器

同步生成器使用sync*关键字标记。它返回一个Iterable<T>对象,你可以使用for-in循环来遍历这些值。每当for-in循环请求下一个值时,生成器函数会执行到下一个yield语句,返回一个值,然后暂停。

// Dart SDK Version: 2.19.0 或更高版本
// 运行环境: Dart VM 或任何支持Dart的平台

// 示例 1: 一个简单的同步生成器
Iterable<int> countUpTo(int limit) sync* {
  for (int i = 1; i <= limit; i++) {
    print('生成器正在产生值: $i'); // 模拟一些工作
    yield i; // 产出当前值,函数暂停
  }
}

// 示例 2: 使用 yield* 委托另一个可迭代对象
Iterable<int> generateEvenNumbers(int start, int end) sync* {
  for (int i = start; i <= end; i++) {
    if (i % 2 == 0) {
      yield i;
    }
  }
}

Iterable<int> allNumbersAndEvenNumbers(int limit) sync* {
  print('开始生成所有数字...');
  yield* countUpTo(limit); // 委托整个 countUpTo 序列

  print('开始生成偶数...');
  yield* generateEvenNumbers(1, limit); // 委托偶数序列
}


void main() {
  print('--- 示例 1: 简单同步生成器 ---');
  Iterable<int> myIterable = countUpTo(3); // 此时生成器函数并未执行内部代码
  
  // 遍历 Iterable 时,生成器才逐步执行
  for (int number in myIterable) {
    print('主程序接收到值: $number');
  }
  // 再次遍历会重新执行生成器
  print('\n--- 再次遍历 ---');
  for (int number in myIterable) {
    print('主程序接收到值: $number');
  }

  print('\n--- 示例 2: 同步生成器与 yield* ---');
  for (int num in allNumbersAndEvenNumbers(5)) {
    print('组合序列接收到值: $num');
  }
}

运行结果示例:

--- 示例 1: 简单同步生成器 ---
生成器正在产生值: 1
主程序接收到值: 1
生成器正在产生值: 2
主程序接收到值: 2
生成器正在产生值: 3
主程序接收到值: 3

--- 再次遍历 ---
生成器正在产生值: 1
主程序接收到值: 1
生成器正在产生值: 2
主程序接收到值: 2
生成器正在产生值: 3
主程序接收到值: 3

--- 示例 2: 同步生成器与 yield* ---
开始生成所有数字...
生成器正在产生值: 1
组合序列接收到值: 1
生成器正在产生值: 2
组合序列接收到值: 2
生成器正在产生值: 3
组合序列接收到值: 3
生成器正在产生值: 4
组合序列接收到值: 4
生成器正在产生值: 5
组合序列接收到值: 5
开始生成偶数...
组合序列接收到值: 2
组合序列接收到值: 4

注意事项:

  • sync*函数必须返回Iterable<T>类型。
  • yield语句只能在sync*async*函数中使用。
  • 每次遍历Iterable时,同步生成器会从头开始重新执行。

异步生成器

理论:
异步生成器使用async*关键字标记。它返回一个Stream<T>对象,用于处理异步事件序列。流在Flutter中非常常见,比如UI事件、网络数据、文件读取等。当你监听一个Stream时,每次生成器通过yield产出一个值,这个值就会作为事件发送给所有监听者。

// Dart SDK Version: 2.19.0 或更高版本
// 运行环境: Dart VM 或任何支持Dart的平台

// 示例 1: 一个简单的异步生成器
Stream<int> countdown(int from) async* {
  for (int i = from; i >= 0; i--) {
    await Future.delayed(Duration(seconds: 1)); // 模拟异步操作
    print('异步生成器正在产生值: $i');
    yield i; // 产出当前值,函数暂停
  }
}

// 示例 2: 结合 await for 消费异步流
Stream<String> fetchUserData(List<String> userIds) async* {
  for (String id in userIds) {
    print('正在为 ID $id 模拟获取数据...');
    await Future.delayed(Duration(milliseconds: 500)); // 模拟网络请求
    yield '用户数据 for ID $id'; // 产出数据
  }
}

// 示例 3: 使用 yield* 委托另一个流
Stream<String> combinedDataStream() async* {
  print('开始获取用户数据...');
  yield* fetchUserData(['userA', 'userB']); // 委托第一个流

  print('开始获取产品数据...');
  await Future.delayed(Duration(seconds: 1));
  yield '产品数据 for ItemX';
  yield '产品数据 for ItemY';
}


void main() async { // main 函数也需要标记为 async 才能使用 await
  print('--- 示例 1: 简单异步生成器 ---');
  Stream<int> myStream = countdown(3); // 此时生成器函数并未执行内部代码
  
  // 订阅 Stream,等待值的到来
  await for (int val in myStream) { // await for 循环用于消费 Stream
    print('主程序接收到值: $val');
  }

  print('\n--- 示例 2: 消费多个异步生成器 ---');
  List<String> ids = ['ID_001', 'ID_002', 'ID_003'];
  await for (String data in fetchUserData(ids)) {
    print('接收到数据: $data');
  }

  print('\n--- 示例 3: 异步生成器与 yield* ---');
  await for (String data in combinedDataStream()) {
    print('组合流接收到: $data');
  }

  print('\n所有流处理完毕。');
}

运行结果示例:

--- 示例 1: 简单异步生成器 ---
异步生成器正在产生值: 3
主程序接收到值: 3
异步生成器正在产生值: 2
主程序接收到值: 2
异步生成器正在产生值: 1
主程序接收到值: 1
异步生成器正在产生值: 0
主程序接收到值: 0

--- 示例 2: 消费多个异步生成器 ---
正在为 ID ID_001 模拟获取数据...
接收到数据: 用户数据 for ID ID_001
正在为 ID ID_002 模拟获取数据...
接收到数据: 用户数据 for ID ID_002
正在为 ID ID_003 模拟获取数据...
接收到数据: 用户数据 for ID ID_003

--- 示例 3: 异步生成器与 yield* ---
开始获取用户数据...
正在为 ID userA 模拟获取数据...
组合流接收到: 用户数据 for ID userA
正在为 ID userB 模拟获取数据...
组合流接收到: 用户数据 for ID userB
开始获取产品数据...
组合流接收到: 产品数据 for ItemX
组合流接收到: 产品数据 for ItemY

所有流处理完毕。

注意事项:

  • async*函数必须返回Stream<T>类型。
  • 可以在async*函数内使用await关键字等待异步操作完成。
  • await for循环是消费Stream的常用方式。

陷阱预警与最佳实践

常见错误模式:

  1. 返回值类型不匹配:
  • sync*函数必须返回Iterable<T>。如果你尝试返回List<T>或其他类型,会报错。
  • async*函数必须返回Stream<T>。如果你尝试返回Future<T>或其他类型,会报错。
  • 错误示例
// void myGenerator() sync* { /* ... */ } // 错误: 返回类型必须是 Iterable
// Future<int> myAsyncGenerator() async* { /* ... */ } // 错误: 返回类型必须是 Stream
  1. 在非生成器函数中使用yield:
  • yield关键字只能出现在被sync*async*标记的函数中。
  • 错误示例:
// int myFunction() {
//   yield 1; // 编译错误
//   return 2;
// }
  1. Iterable进行修改时的问题:
  • 如果你在遍历的过程中修改了生成器函数内部依赖的数据源,可能导致不可预测的行为。生成器是惰性求值的,数据源最好是稳定的。
  1. 忘记await for消费Stream:
  • 异步生成器返回的是Stream,如果你只是调用了函数,但没有订阅或使用await for来消费它,那么生成器内部的代码根本不会执行。
  • 错误示例:
// countdown(3); // 这样调用不会有任何输出

生产环境最佳实践:

  1. 无限序列的限制: 如果你的生成器创建了一个无限序列,请确保在消费时有明确的终止条件或取用数量限制,以避免无限循环和内存耗尽。
Iterable<int> fibonacci() sync* {
  int a = 0;
  int b = 1;
  while (true) {
    yield a;
    int next = a + b;
    a = b;
    b = next;
  }
}

void main() {
  // bad: 可能会导致无限循环
  // for(var n in fibonacci()) { print(n); } 

  // good: 使用 take 限制数量
  for(var n in fibonacci().take(10)) {
    print(n); // 打印前10个斐波那契数
  }
}
  1. 错误处理:
  • 对于同步生成器,如果函数内部抛出异常,通常会在遍历到异常点时抛出。
  • 对于异步生成器(Stream),可以使用Stream.handleError.catchError方法来处理流中的错误事件。
Stream<int> riskyCountdown(int from) async* {
  for (int i = from; i >= 0; i--) {
    await Future.delayed(Duration(milliseconds: 500));
    if (i == 1) {
      throw Exception('Something went wrong at 1!');
    }
    yield i;
  }
}

void main() async {
  print('\n--- 异步生成器错误处理 ---');
  await for (int val in riskyCountdown(3).handleError((e) {
    print('捕获到错误: $e');
  })) {
    print('接收到值: $val');
  }
}
  1. 结合Flutter: 在Flutter中,Stream是响应式编程的核心。StreamBuilder widget就是利用Stream来自动更新UI。异步生成器是创建这些Stream的强大工具。