为何C#中合并含yield的异步方法会触发编译错误?
为什么合并异步方法和迭代器会编译失败?
先看这段可以正常编译的代码:
public async Task<IEnumerable<T>> GetResultsFromQueryExecutionId<T>(string queryExecutionId) { await using var csvResponseStream = await transferUtility.OpenStreamAsync("bucket", "blah.csv"); return GetResultsFromResponseStream<T>(csvResponseStream); } private IEnumerable<T> GetResultsFromResponseStream<T>(Stream csvResponseStream) { using var streamReader = new StreamReader(csvResponseStream); using var csvReader = new CsvReader(streamReader, csvConfiguration); foreach (var record in csvReader.GetRecords<T>()) { yield return record; } }
但把逻辑合并到单个异步方法后,就会触发编译错误:
The body of 'GetResultsFromQueryExecutionId' cannot be an iterator block because 'System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable
>' is not an async iterator interface type
合并后的代码:
public async Task<IEnumerable<T>> GetResultsFromQueryExecutionId<T>(string queryExecutionId, string logPrefix = null) { await using var csvResponseStream = await transferUtility.OpenStreamAsync("bucket", "blah.csv"); using var streamReader = new StreamReader(csvResponseStream); using var csvReader = new CsvReader(streamReader, csvConfiguration); foreach (var record in csvReader.GetRecords<T>()) { yield return record; } }
核心原因:异步方法和迭代器方法的规则冲突
C#里对这两种方法的规则是严格分开的:
- 普通迭代器方法:只要用了
yield return/yield break,返回类型只能是IEnumerable<T>、IEnumerator<T>、IEnumerable、IEnumerator这四种。编译器会自动生成对应的迭代器实现类,和Task完全无关。 - 异步方法:带
async关键字的方法,返回类型只能是Task、Task<T>、ValueTask、ValueTask<T>或者void(不推荐)。编译器会生成异步状态机来处理await逻辑,不允许同时处理迭代器的yield逻辑。
合并后的代码同时违反了这两条规则:
- 加了
async关键字后,编译器把它当成异步方法,但又在里面用了yield return,试图让它同时做迭代器。 - 返回类型
Task<IEnumerable<T>>既不是异步方法的合法泛型返回(异步方法的Task<T>里的T必须是普通类型,不能是迭代器接口),也不是异步迭代器的合法返回类型。
为什么原来的拆分代码没问题?
拆分的代码里,两个方法各司其职:
- 外层异步方法只负责处理异步操作(
await OpenStreamAsync),然后返回一个普通的迭代器对象(IEnumerable<T>)。 - 内层方法是纯迭代器,只处理
yield return逻辑,和异步完全不沾边。
编译器分别为异步方法生成异步状态机,为迭代器方法生成迭代器实现,两者互不干扰,自然能正常编译。
正确的合并方式:用异步迭代器
如果要把逻辑合并,得用C#的异步迭代器特性,把返回类型改成IAsyncEnumerable<T>,同时保留async关键字:
public async IAsyncEnumerable<T> GetResultsFromQueryExecutionId<T>(string queryExecutionId, string logPrefix = null) { await using var csvResponseStream = await transferUtility.OpenStreamAsync("bucket", "blah.csv"); using var streamReader = new StreamReader(csvResponseStream); using var csvReader = new CsvReader(streamReader, csvConfiguration); // 注意:如果要真正异步读取,建议用CsvHelper的异步API await foreach (var record in csvReader.GetRecordsAsync<T>()) { yield return record; } }
(注:上面用了GetRecordsAsync和await foreach,这才是真正的异步迭代读取,比同步迭代包装成异步更合理)
内容的提问来源于stack exchange,提问作者gregdev
相关产品推荐
相关产品推荐

