在C#8之前,可以使用yield return
实现迭代器,也可以用await
书写异步函数。但无法两者结合,实现一个可以等待的迭代器。C#8引入了异步流解决了这个问题。
异步流基于以下两个接口。
public interface IAsyncEnumerable<out T> { IAsyncEnumerator<T> GetAsyncEnumerator(...); } public interface IAsyncEnumerator<out T> : IAsyncDisposable { T Current { get; } ValueTask<bool> MoveNextAsync(); }
ValueTask<T>
结构体封装了Task<T>
,其行为和Task<T>
相似。在任务对象同步完成的情况下具有更好的执行性能。
结合使用迭代器和异步方法的规则来书写方法就可以创建一个异步流。即该方法应同时具备yield return
和await
,并且返回值为IAsyncEnumerable<T>
:
async IAsyncEnumerable<int> RangeAsync ( int start, int count, int delay) { for (int i = start; i < start + count; i++) { await Task.Delay (delay); yield return i; } }
相应地,使用await foreach
语句就可以消费异步流:
await foreach (var number in RangeAsync (0, 10, 500)) { Console.WriteLine (number); }
以上例子,数据持续以每0.5秒一个的速度到达。
而如果项以下例子中使用Task<IEnumerable<T>>
,则只有所有数据都准备好时才会最终返回:
static async Task<IEnumerable<int>> RangeTaskAsync(int start, int count, int delay) { List<int> data = new List<int>(); for (int i = start; i < start + count; i++) { await Task.Delay (delay); data.Add (i); } return data; }
消费上例中的结果只需使用foreach
语句即可:
foreach (var data in await RangeTaskAsync(0, 10, 500)) { Console.WriteLine (data); }
IAsyncEnumerable<T>
System.Linq.Async Nuget
包包含了对IAsyncEnumerable<T>
查询的LINQ
查询运算符。这样就可以像查询IEnumerable<T>
那样书写查询了。
例如,可以对前面RangeAsync
方法书写LINQ查询:
IAsyncEnumerable<int> query = from i in RangeAsync (0, 10, 500) where i % 2 == 0 select i * 10; await foreach (var number in query) { Console.WriteLine (number); }
ASP.NET Core
中的IAsyncEnumerable<T>
ASP.NET Core
控制器的方法目前支持返回IAsyncEnumerable<T>
对象了,此类方法必须标记async
:
[HttpGet] public async IAsyncEnumerable<string> Get() { using var dbContext = new DbContext(); await foreach (var title in dbContext.Books .Select(b=>b.Title).AsAsyncEnumerable()) { yield return title; } }