一、什么是协程和生成器

协程就像是一个可以随时暂停和继续的函数。想象你在看电视剧,看到一半有事离开,回来时可以接着上次的地方继续看,协程就是这样的存在。而生成器是协程的一种实现方式,它能在执行过程中产生多个值,而不是一次性返回所有结果。

在Dart中,生成器通过sync*async*关键字实现。sync*用于同步生成器,async*用于异步生成器。今天我们要重点讨论的是异步生成器,因为它特别适合处理数据流。

二、Dart中的异步生成器

异步生成器最大的特点就是可以逐步产生数据,而不用等所有数据都准备好。这在处理网络请求、文件读取等IO密集型操作时特别有用。

下面是一个简单的异步生成器示例:

// 技术栈:Dart
// 模拟从网络异步获取数据的生成器
Stream<int> fetchData() async* {
  for (int i = 1; i <= 5; i++) {
    // 模拟网络延迟
    await Future.delayed(Duration(seconds: 1));
    // 每次产生一个数字
    yield i;
  }
}

void main() async {
  // 使用生成器
  await for (final number in fetchData()) {
    print('收到数据: $number');
  }
  print('数据接收完成');
}

这个例子中,fetchData是一个异步生成器,它会每隔一秒产生一个数字。yield关键字类似于return,但它不会结束函数执行,而是把值发送出去后继续执行。

三、处理真实场景的数据流

让我们看一个更贴近实际的例子:从API分页获取数据。

// 技术栈:Dart
// 分页获取数据的异步生成器
Stream<List<Map<String, dynamic>>> fetchPaginatedData(String apiUrl) async* {
  int page = 1;
  bool hasMore = true;
  
  while (hasMore) {
    // 模拟API请求
    final response = await http.get(Uri.parse('$apiUrl?page=$page'));
    final data = jsonDecode(response.body);
    
    // 返回当前页数据
    yield data['items'];
    
    // 检查是否还有更多数据
    hasMore = data['hasMore'];
    page++;
    
    // 避免请求过于频繁
    await Future.delayed(Duration(milliseconds: 500));
  }
}

void main() async {
  const apiUrl = 'https://example.com/api/data';
  
  // 使用生成器处理分页数据
  await for (final pageItems in fetchPaginatedData(apiUrl)) {
    print('收到一页数据,共${pageItems.length}条');
    // 处理数据...
  }
  
  print('所有数据加载完成');
}

这个例子展示了如何使用异步生成器优雅地处理分页数据。相比传统的回调方式,代码更加清晰易读。

四、生成器的高级用法

异步生成器还可以和其他Dart特性结合使用,比如错误处理和流转换。

// 技术栈:Dart
// 带错误处理的异步生成器
Stream<String> fetchWithRetry(String url, int maxRetries) async* {
  int retryCount = 0;
  
  while (true) {
    try {
      final response = await http.get(Uri.parse(url));
      if (response.statusCode == 200) {
        yield response.body;
        break;
      } else {
        throw Exception('请求失败: ${response.statusCode}');
      }
    } catch (e) {
      if (retryCount >= maxRetries) rethrow;
      retryCount++;
      print('请求失败,第$retryCount次重试...');
      await Future.delayed(Duration(seconds: 1));
    }
  }
}

// 流转换示例
void processData() async {
  final stream = fetchWithRetry('https://example.com/api', 3)
      .transform(utf8.decoder)  // 字节转字符串
      .transform(LineSplitter()); // 按行分割
  
  await for (final line in stream) {
    print('处理行数据: $line');
  }
}

五、应用场景分析

异步生成器特别适合以下场景:

  1. 分页加载数据
  2. 实时数据流处理(如WebSocket)
  3. 大文件逐行读取
  4. 需要重试机制的请求
  5. 复杂的数据转换流水线

六、技术优缺点

优点:

  1. 代码更简洁,避免了回调地狱
  2. 内存效率高,不需要一次性加载所有数据
  3. 可以轻松实现复杂的数据处理流程
  4. 天然支持异步操作

缺点:

  1. 学习曲线稍陡,需要理解生成器概念
  2. 调试可能比同步代码困难
  3. 错误处理需要特别注意

七、注意事项

  1. 记得在生成器中使用try-catch处理错误
  2. 避免在生成器中执行耗时操作,会阻塞整个流
  3. 注意流的生命周期管理,避免内存泄漏
  4. 考虑使用StreamController来创建更复杂的流
  5. 对于热流(hot stream)和冷流(cold stream)的区别要有清晰认识

八、总结

Dart的异步生成器提供了一种优雅的方式来处理数据流。通过async*yield的组合,我们可以创建出既高效又易读的异步代码。虽然它有一些学习门槛,但一旦掌握,就能大幅提升异步编程的效率和质量。

在实际项目中,建议从小规模开始尝试,逐步应用到更复杂的场景。记住,生成器不是万能的,但对于适合的场景,它能带来显著的代码质量提升。