C# 中 的 IAsyncEnumerable
一、IAsyncEnumerable<T> 是什么?
一句话解释:
它是一种异步数据流,让你可以像处理列表一样逐个处理数据,但数据是按需异步生成的。
类比生活场景:
想象你在餐厅点菜:
- 同步枚举(
IEnumerable):服务员把整桌菜都做好端上来,你才能开始吃。 - 异步枚举(
IAsyncEnumerable):服务员做好一道菜就端上来一道,你可以边吃边等下一道。
二、为什么需要它?
典型场景:
- 处理大数据:比如读取10GB的日志文件,不用一次性加载到内存。
- 远程数据:比如从API分页获取数据,不用等所有页都下载完。
- 实时数据流:比如监控传感器数据,数据是持续产生的。
三、如何使用?
1. 基本语法
// 定义一个异步生成数据的方法
async IAsyncEnumerable<int> GenerateNumbersAsync()
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100); // 模拟异步操作(比如网络请求)
yield return i; // 返回一个数据项
}
}
// 消费这个异步数据流
await foreach (var number in GenerateNumbersAsync())
{
Console.WriteLine(number); // 逐个处理数据,不用等全部生成
}
关键语法点:
async IAsyncEnumerable<T>:方法返回类型,表示这是一个异步数据流。yield return:每次产生一个数据项,而不是一次性返回整个列表。await foreach:异步遍历数据流,每次等待一个数据项就绪。
2. 实际例子:从文件异步读取数据
// 读取大文件的每一行,不用一次性加载整个文件
async IAsyncEnumerable<string> ReadLinesAsync(string filePath)
{
await using var reader = new StreamReader(filePath);
string line;
while ((line = await reader.ReadLineAsync()) != null)
{
yield return line; // 读到一行就返回一行
}
}
// 使用方式
await foreach (var line in ReadLinesAsync("large_file.txt"))
{
Console.WriteLine($"处理行:{line.Length} 字符");
}
3. 处理异步API请求(分页数据)
假设你要从API获取用户列表(每页10个用户):
async IAsyncEnumerable<User> GetUsersAsync()
{
int page = 1;
List<User> currentPage;
do
{
// 模拟API请求,每次获取一页数据
currentPage = await FetchUsersFromApiAsync(page);
page++;
// 把当前页的用户逐个返回
foreach (var user in currentPage)
{
yield return user;
}
} while (currentPage.Count > 0); // 还有下一页数据
}
// 使用方式
await foreach (var user in GetUsersAsync())
{
Console.WriteLine($"用户:{user.Name}");
}
四、常见问题
1. 它和 Task<List<T>> 有什么区别?
Task<List<T>>:等待所有数据加载完成,返回完整列表(适合小数据)。IAsyncEnumerable<T>:边加载边处理,适合大数据或实时数据流。
2. 如何取消异步枚举?
可以传入 CancellationToken:
async IAsyncEnumerable<int> GenerateNumbersAsync(
[EnumeratorCancellation] CancellationToken token = default)
{
for (int i = 0; i < 1000; i++)
{
token.ThrowIfCancellationRequested(); // 检查是否取消
await Task.Delay(100, token);
yield return i;
}
}
// 使用时可以取消
var cts = new CancellationTokenSource();
cts.CancelAfter(500); // 500ms后自动取消
await foreach (var number in GenerateNumbersAsync(cts.Token))
{
Console.WriteLine(number);
}
五、总结
核心思想:
IAsyncEnumerable<T> 让你可以异步地逐个处理数据,而不是等待所有数据准备好。这在处理大数据、远程数据或实时数据流时非常有用。
使用步骤:
- 定义异步生成器:用
async IAsyncEnumerable<T>和yield return返回数据。 - 消费数据流:用
await foreach逐个处理数据。 - 按需添加取消逻辑:通过
CancellationToken控制异步操作。
✅ 用法结构总结一下:
🔹 定义阶段(生产数据)
public async IAsyncEnumerable<T> GetDataAsync(
[EnumeratorCancellation] CancellationToken token = default)
{
while (/* 有更多数据 */)
{
token.ThrowIfCancellationRequested(); // 取消支持
await Task.Delay(100); // 异步等待/IO操作
yield return someData; // 返回数据
}
}
🔹 使用阶段(消费数据)
var cts = new CancellationTokenSource();
cts.CancelAfter(5000); // 或者在外部通过按钮控制取消
await foreach (var item in GetDataAsync(cts.Token))
{
Console.WriteLine(item);
}
🔁 替换为你自己的类型举例
假设你定义了一个结构体:
public struct ChannelSample
{
public DateTime Timestamp { get; set; }
public float Value { get; set; }
}
然后你可以这么用:
public async IAsyncEnumerable<ChannelSample> ReadSamplesAsync(
[EnumeratorCancellation] CancellationToken token = default)
{
for (int i = 0; i < 1000; i++)
{
token.ThrowIfCancellationRequested();
await Task.Delay(10, token); // 模拟采集
yield return new ChannelSample
{
Timestamp = DateTime.Now,
Value = i * 0.1f
};
}
}
消费它:
await foreach (var sample in ReadSamplesAsync(cts.Token))
{
Console.WriteLine($"{sample.Timestamp}: {sample.Value}");
}
❗关于 [EnumeratorCancellation] 参数说明
这不是必须的,但推荐加上(尤其在 .NET Core 3.0+ / .NET 5+ 中),否则取消令牌可能不会正常生效。
- 它告诉编译器/运行时:“这个 token 是给异步枚举器用的”
- 否则你只能在调用时显式用
.WithCancellation(token)来加 token
例子对比:
// 推荐方式(有 EnumeratorCancellation)
await foreach (var item in GetDataAsync(token)) { ... }
// 不加时要这样
await foreach (var item in GetDataAsync().WithCancellation(token)) { ... }
🧪 总结一下关键点
| 要素 | 说明 |
|---|---|
async IAsyncEnumerable<T> |
定义一个异步生成器 |
await foreach |
异步逐条读取 |
yield return |
一条一条产生数据 |
[EnumeratorCancellation] |
支持 await foreach 的取消 |
CancellationToken |
控制中止,如用户点击“停止采集” |

浙公网安备 33010602011771号