如何在非异步IAsyncEnumerable枚举后正确处置实例化的可释放对象?
问题:IAsyncEnumerable异步读取文件时正确处置StreamReader的方案
我需要通过IAsyncEnumerable<string>异步公开从文件读取并解析字符串的功能,让调用方可以逐步处理数据。但现在遇到一个问题:必须把StreamReader这类可释放对象传给解析器,但解析器不能在使用后处置它。
现有Wrapper类的ReadStringsAsync方法里用using var reader声明StreamReader,会在返回IAsyncEnumerable前就释放对象,导致抛出System.ObjectDisposedException异常。
需要重构非异步的IAsyncEnumerable<string> Wrapper.ReadStringsAsync()方法,满足以下要求:
- 可通过
await foreach()调用 - 向调用方隐藏
StreamReader的使用 - 仍由
Wrapper持有StreamReader,可在返回IAsyncEnumerable前调用其方法,并在使用后处置 - 不改为异步方法(因为无法使用
yield return) - 仅在枚举完所有内容后才处置
StreamReader
我试过两种方案但都有顾虑:
- 将方法改为异步,通过
await foreach遍历解析器返回的结果并yield return,但担心多余状态机带来性能开销 - 通过
out参数返回可释放对象,由调用方手动处置,但修改了方法签名,不利于后续重构
想知道如何在不生成多余状态机且不修改方法签名的前提下,正确处置该可释放对象?
解决方案:自定义实现IAsyncEnumerable和IAsyncEnumerator
核心思路是自己实现IAsyncEnumerable<string>和IAsyncEnumerator<string>接口,在枚举器的DisposeAsync方法中处置StreamReader,这样就能保证只有在枚举完成或中途取消时才释放资源,同时不需要修改原方法签名,也不会产生额外的状态机开销。
步骤1:实现自定义异步枚举器
public class StringReaderAsyncEnumerator : IAsyncEnumerator<string> { private readonly StreamReader _reader; private readonly IAsyncEnumerator<string> _parserEnumerator; public StringReaderAsyncEnumerator(StreamReader reader, IAsyncEnumerator<string> parserEnumerator) { _reader = reader; _parserEnumerator = parserEnumerator; } public string Current => _parserEnumerator.Current; public async ValueTask<bool> MoveNextAsync() { return await _parserEnumerator.MoveNextAsync(); } public async ValueTask DisposeAsync() { // 先释放解析器的枚举器 await _parserEnumerator.DisposeAsync(); // 再释放StreamReader _reader.Dispose(); GC.SuppressFinalize(this); } }
步骤2:实现自定义异步可枚举对象
public class StringReaderAsyncEnumerable : IAsyncEnumerable<string> { private readonly StreamReader _reader; private readonly Func<StreamReader, IAsyncEnumerable<string>> _parser; public StringReaderAsyncEnumerable(StreamReader reader, Func<StreamReader, IAsyncEnumerable<string>> parser) { _reader = reader; _parser = parser; } public IAsyncEnumerator<string> GetAsyncEnumerator(CancellationToken cancellationToken = default) { var parserEnumerable = _parser(_reader); return new StringReaderAsyncEnumerator(_reader, parserEnumerable.GetAsyncEnumerator(cancellationToken)); } }
步骤3:修改Wrapper类的ReadStringsAsync方法
public class Wrapper { public IAsyncEnumerable<string> ReadStringsAsync(string filePath) { // 这里不使用using,因为要把StreamReader交给自定义枚举器处置 var reader = new StreamReader(filePath); // 假设ParseAsync是你的解析方法,接收StreamReader返回IAsyncEnumerable<string> return new StringReaderAsyncEnumerable(reader, ParseAsync); } // 示例解析方法 private IAsyncEnumerable<string> ParseAsync(StreamReader reader) { // 这里实现你的解析逻辑,比如逐行读取并处理 return reader.ReadLinesAsync(); } }
方案优势说明
- 支持await foreach调用:自定义的
IAsyncEnumerable完全符合.NET异步枚举规范,调用方可以直接用await foreach遍历 - 隐藏StreamReader:调用方只接触
IAsyncEnumerable<string>,完全不知道内部StreamReader的存在 - Wrapper控制资源生命周期:
StreamReader由Wrapper创建,最终在自定义枚举器的DisposeAsync中被释放,全程由Wrapper把控 - 无需修改方法签名:
ReadStringsAsync依然返回IAsyncEnumerable<string>,和原签名完全一致 - 无额外状态机开销:自定义枚举器直接封装解析器的枚举器,没有多余的异步状态机,性能和直接调用解析器几乎无差异
额外注意事项
- 异常场景下资源仍会被释放:
await foreach在遇到异常时会自动调用枚举器的DisposeAsync,不会出现资源泄漏 - 支持取消操作:记得在
GetAsyncEnumerator中传递CancellationToken给解析器的枚举器,实现取消逻辑
内容的提问来源于stack exchange,提问作者CodeCaster
相关产品推荐
相关产品推荐

