第十一章:C#异步函数与面向对象编程的融合
第十一章:C#异步函数与面向对象编程的融合
在现代软件开发中,异步编程已经成为提升应用程序性能和用户体验的关键技术。然而,C# 的异步特性是函数式编程范式的产物,与传统面向对象编程存在本质上的差异。当需要将异步操作融入面向对象的设计中时,你可能会遇到许多独特的挑战,例如接口方法的异步化、构造函数中的异步逻辑处理、异步属性与事件的设计等。
11.1 异步接口及继承
问题
在接口或基类中定义一个方法时,如何将其异步化?
解决方案
核心理念是:async 是一个实现细节,不能直接用于接口或抽象方法的定义,但可以通过返回 Task 或 Task<T> 来定义异步的签名。
- 接口方法或抽象方法:直接返回
Task或Task<T>,表示其实现可能是异步的。 - 实现方法:在实现时使用
async关键字完成具体的异步操作。
这样,异步接口的设计兼容同步实现和异步实现,也符合灵活性和规范性的需求。
代码示例
- 定义接口
接口通过返回 Task 或 Task<T> 的方式定义“异步方法”。这样的方法本质上是“可等待的”,可以通过 await 使用:
| interface IMyAsyncInterface | |
| { | |
| Task<int> CountBytesAsync(HttpClient client, string url); | |
| } |
- 异步实现
类实现接口,并在方法体内使用async/await执行异步操作:
| class MyAsyncClass : IMyAsyncInterface | |
| { | |
| public async Task<int> CountBytesAsync(HttpClient client, string url) | |
| { | |
| var bytes = await client.GetByteArrayAsync(url);// 异步操作 | |
| return bytes.Length; | |
| } | |
| } |
- 消费异步接口
在使用该接口时,可以直接通过 await 调用其方法:
| async Task UseMyInterfaceAsync(HttpClient client, IMyAsyncInterface service) | |
| { | |
| var result = await service.CountBytesAsync(client, "http://example.com"); | |
| Console.WriteLine(result); | |
| } |
- 同步实现
为了方便测试或兼容需求,接口的实现方法可以同步返回结果,而不需要实际执行异步操作:
| class MyAsyncClassStub : IMyAsyncInterface | |
| { | |
| public Task<int> CountBytesAsync(HttpClient client, string url) | |
| { | |
| // 返回一个固定的结果,而无需异步计算 | |
| return Task.FromResult(42); | |
| } | |
| } |
注意:如下这种也是普通同步方法,因为方法并没有异步调用任何方法,只是创建并启动了一个Task,然后返回这个Task而已,所以是同步方法(编译器不会为
DoSomethingAsync生成状态机):
public Task<int> DoSomethingAsync() { return Task.Run(async () => { await Task.Delay(1000); return 5; }); } 但是如果是这样写,那就是异步方法了(编译器会为
DoSomethingAsync生成状态机):
public async Task<int> DoSomethingAsync() { return await Task.Run(async () => { await Task.Delay(1000); return 5; }); }
小结
-
接口方法不能包含
async关键字:async是方法实现的一部分,用于告诉编译器生成状态机来处理异步操作,而接口和抽象类仅定义方法的签名,不涉及实现细节。因此,接口和抽象类的方法不能使用async。
-
异步接口的本质:
- 异步接口的核心在于返回类型是
Task或Task<T>,而不是方法本身是否使用async。 - 调用方只需知道方法返回可等待的任务,而无需关心具体实现是异步的还是同步的。
- 异步接口的核心在于返回类型是
-
异步任务的本质:
- 一个
Task或Task<T>代表一项任务,可能是未完成的任务(将在未来完成)或已完成的任务(立即返回结果)。 - 通过这种机制,调用方可以统一使用
await等待任务的完成,而不需要感知任务的具体状态。
- 一个
-
实现的灵活性:
- 接口方法可以通过真正的异步操作(如
async和await)实现,也可以通过同步方式(如Task.FromResult)实现。 - 这种灵活性允许在不同场景下选择合适的实现策略:
- 如果存在实际的异步操作(如 I/O、网络请求),应采用真正的异步实现。
- 如果不需要异步操作,可以返回一个已完成的任务(例如
Task.FromResult)。
- 接口方法可以通过真正的异步操作(如
-
异步化策略:
- 优先异步:当方法涉及 I/O 操作(如文件读写、网络请求)时,应优先采用异步实现,以提升性能并避免阻塞线程。
- 避免不必要的
async修饰:如果方法没有实际的异步工作(例如只是返回固定结果),应避免添加不必要的async修饰,直接返回任务(如Task.FromResult),以提高性能。
11.2 异步构造方法:工厂模式
问题
在某些场景下,我们需要在对象的构造过程中执行异步操作(例如加载配置文件、从远程服务拉取数据等)。然而,C# 的构造函数不支持 async 或 await,因为构造函数本身无法返回 Task。
解决方案:异步工厂模式
一种优雅的解决方案是使用异步工厂方法模式,让类自身提供一个静态的异步工厂方法来创建和初始化实例。这种方法将初始化的异步逻辑与实例创建绑定在一起,确保只有在完成初始化后才可以访问实例。
以下是一个完整的异步工厂模式代码示例:
| using System; | |
| using System.Threading.Tasks; | |
| class MyAsyncClass | |
| { | |
| // 私有构造器,防止直接实例化 | |
| private MyAsyncClass() | |
| { | |
| Console.WriteLine("Constructor called"); | |
| } | |
| // 异步初始化方法,私有 | |
| private async Task<MyAsyncClass> InitializeAsync() | |
| { | |
| Console.WriteLine("Initializing asynchronously..."); | |
| await Task.Delay(1000); // 模拟异步操作 | |
| Console.WriteLine("Initialization complete"); | |
| return this; | |
| } | |
| // 静态工厂方法 | |
| public static Task<MyAsyncClass> CreateAsync() | |
| { | |
| var instance = new MyAsyncClass(); | |
| return instance.InitializeAsync(); | |
| } | |
| } | |
| class Program | |
| { | |
| static async Task Main(string[] args) | |
| { | |
| Console.WriteLine("Creating instance..."); | |
| MyAsyncClass instance = await MyAsyncClass.CreateAsync(); | |
| Console.WriteLine("Instance created and ready to use!"); | |
| } | |
| } |
执行结果:
| Creating instance... | |
| Constructor called | |
| Initializing asynchronously... | |
| Initialization complete | |
| Instance created and ready to use! |
设计要点:
- 构造函数和
InitializeAsync方法为私有,防止直接调用。 - 通过静态工厂方法
CreateAsync创建实例,并确保初始化完成后再返回对象。
常见问题与反例
反例:在构造器中启动异步操作
| class MyAsyncClass | |
| { | |
| public MyAsyncClass() | |
| { | |
| InitializeAsync(); | |
| } | |
| // 错误:使用 async void | |
| private async void InitializeAsync() | |
| { | |
| await Task.Delay(TimeSpan.FromSeconds(1)); // 模拟异步操作 | |
| } | |
| } |
问题:
- 未完成的实例: 构造函数返回时,实例的异步初始化尚未完成,调用方可能误用未-准备好的实例。
- 异常无法捕获:由于 async void 的特殊性,InitializeAsync 方法中抛出的异常无法通过构造函数外的 try-catch 捕获。
- 不确定的状态:调用方无法得知异步操作何时完成。
异步工厂模式的优点
-
安全性:
- 确保实例在初始化完成后才可用,避免未初始化实例被错误使用。
-
封装性:
- 初始化逻辑被封装在工厂方法中,调用方无需关心实现细节。
-
可维护性:
- 异步工厂模式强制调用者按正确的方式使用类型,减少潜在错误。
异步工厂模式的局限性
尽管异步工厂模式是解决异步构造问题的推荐方法,但在某些场景中可能会面临以下局限性:
-
与依赖注入框架的不兼容:
-
大多数依赖注入(DI)框架(例如 ASP.NET Core 的 DI 容器)无法处理异步工厂方法。这是因为 DI 容器通常会直接调用构造器来创建对象,而无法等待异步工厂方法。
-
解决方案:
- 如果初始化是共享资源,可以使用惰性初始化(如
AsyncLazy)。 - 如果必须支持异步初始化,可以参考下一节的异步初始化模式。
- 如果初始化是共享资源,可以使用惰性初始化(如
-
-
代码复杂性:
- 对于简单的类型,异步工厂模式可能显得过于复杂。如果初始化逻辑非常简单,可以考虑让调用方显式调用异步初始化方法(尽管可能存在调用方忘记调用的问题)。
11.3 异步构造:异步初始化模式
问题
在某些情况下,无法使用工厂模式(参见 11.2 节),例如实例是通过以下方式创建的:
依赖注入(DI)容器:例如 ASP.NET Core 中的 DI。反射:如 Activator.CreateInstance。数据绑定。
此时我们需要一种机制来支持异步初始化,同时避免初始化过程中导致实例状态不一致。
解决方案:异步初始化模式
异步初始化模式的核心思想是:
- 在构造器中启动异步初始化,并将初始化的任务暴露为一个公共属性
Initialization。 - 调用方可以通过检查并等待
Initialization属性,确保实例完成初始化后再使用。
代码示例
1. 定义标记接口
为了规范所有需要异步初始化的类型,可以定义一个接口来强制实现 Initialization 属性:
| /// <summary> | |
| /// 标记需要异步初始化的类型,并提供初始化任务 | |
| /// </summary> | |
| public interface IAsyncInitialization | |
| { | |
| /// <summary> | |
| /// 异步初始化任务 | |
| /// </summary> | |
| Task Initialization { get; } | |
| } |
2. 实现基本类型
| public class MyFundamentalType : IAsyncInitialization | |
| { | |
| public MyFundamentalType() | |
| { | |
| // 构造函数中启动异步初始化 | |
| Initialization = InitializeAsync(); | |
| } | |
| public Task Initialization { get; private set; } | |
| private async Task InitializeAsync() | |
| { | |
| // 模拟异步初始化 | |
| await Task.Delay(TimeSpan.FromSeconds(1)); | |
| } | |
| } |
3. 使用场景
通过依赖注入框架或反射创建实例:
| IMyFundamentalType instance = UltimateDIFactory.Create<IMyFundamentalType>(); | |
| if (instance is IAsyncInitialization instanceAsyncInit) | |
| { | |
| // 等待异步初始化完成 | |
| await instanceAsyncInit.Initialization; | |
| } |
4. 合成类型支持
合成类型可能依赖多个需要异步初始化的组件:
| public class MyComposedType : IAsyncInitialization | |
| { | |
| private readonly IMyFundamentalType _fundamental; | |
| public MyComposedType(IMyFundamentalType fundamental) | |
| { | |
| _fundamental = fundamental; | |
| Initialization = InitializeAsync(); | |
| } | |
| public Task Initialization { get; private set; } | |
| private async Task InitializeAsync() | |
| { | |
| // 如果组件实现了异步初始化,则等待其完成 | |
| if (_fundamental is IAsyncInitialization fundamentalInit) | |
| { | |
| await fundamentalInit.Initialization; | |
| } | |
| // 执行自身的初始化逻辑 | |
| await Task.Delay(TimeSpan.FromSeconds(1)); | |
| } | |
| } |
5. 简化多组件初始化
可通过辅助方法简化对多个组件的初始化检查:
| public static class AsyncInitialization | |
| { | |
| public static Task WhenAllInitializedAsync(params object[] instances) | |
| { | |
| return Task.WhenAll(instances | |
| .OfType<IAsyncInitialization>() // 筛选出需要异步初始化的实例 | |
| .Select(x => x.Initialization)); // 收集初始化任务 | |
| } | |
| } | |
| // 示例:合成类型依赖多个注入组件 | |
| private async Task InitializeAsync() | |
| { | |
| await AsyncInitialization.WhenAllInitializedAsync(_fundamental, _anotherType, _yetAnother); | |
| // 自身的初始化逻辑 | |
| } |
优缺点分析
优点
- 兼容性强:支持反射创建、依赖注入、数据绑定等场景。
- 异步初始化管理清晰:通过
Initialization属性集中管理异步状态。 - 灵活性:初始化可异步也可同步完成,允许根据具体情况实现。
缺点
- 暴露未初始化实例:与异步工厂模式不同,该模式允许调用方在初始化未完成时访问实例,可能导致使用未完全准备好的实例。
- 复杂性:当组件依赖关系复杂时,需要额外代码管理依赖的初始化顺序和状态。
最佳实践
- 优先使用工厂模式:如果可能,尽量采用 11.2 节中的异步工厂模式,避免直接暴露未初始化的实例。
- 谨慎依赖异步初始化:减少对异步初始化的依赖,优先设计为延迟初始化。
- 辅助工具类简化逻辑:对于复杂依赖关系,使用辅助方法(如
AsyncInitialization.WhenAllInitializedAsync)来减少冗余代码。
11.4 异步属性
问题
在实际开发中,可能会遇到需要将某个属性转换为异步操作的场景。比如,当属性的 getter 方法需要执行异步操作时,如何正确地处理?然而,C# 中并没有异步属性的概念(即 async 属性),也无法直接在属性中使用 async 关键字。这种限制实际上是有意义的,因为属性的设计语义是用于快速获取数据,而不是启动复杂的后台操作。
以下是一个典型的错误示例(代码无法编译):
| // 错误:尝试将属性变为异步 | |
| public int Data | |
| { | |
| async get | |
| { | |
| await Task.Delay(TimeSpan.FromSeconds(1)); | |
| return 13; | |
| } | |
| } |
因此,当发现需要“异步属性”时,实际上应该重新审视设计思路,根据场景选择合适的实现方式。
解决方案:两种设计选择
在需要异步属性的场景中,通常存在两种需求:
- 每次访问属性时,重新启动异步计算。
- 只进行一次异步计算并缓存结果,之后的访问返回相同的值。
根据这两种需求,可以分别采用以下解决方案:
方案 1:属性值需要重复异步计算
在这种情况下,属性的行为本质上是一个异步方法,因为每次访问属性时都会重新启动异步操作。这种设计不适合用属性实现,而应该改为显式的异步方法。
示例:
| // 使用异步方法代替属性 | |
| public async Task<int> GetDataAsync() | |
| { | |
| await Task.Delay(TimeSpan.FromSeconds(1)); // 模拟异步操作 | |
| return 13; | |
| } |
调用代码:
| int value = await instance.GetDataAsync(); |
注意:虽然也可以通过属性返回 Task<T>,如下所示:
| // 返回 Task<T> 的属性 | |
| public Task<int> Data | |
| { | |
| get { return GetDataAsync(); } | |
| } |
但是,这种设计会让 API 产生误导。调用方在读取 Data 属性时,可能会误以为是普通的同步属性,而不清楚其行为是异步的。因此,不推荐使用这种方式。
方案 2:异步计算值并缓存
如果属性值只需要异步计算一次,并在后续访问中返回相同的结果,可以使用异步延迟初始化的方式。这种方式非常适合用属性来实现,并且符合属性的语义。
示例:
| public AsyncLazy<int> Data { get; } | |
| private readonly AsyncLazy<int> _data = new AsyncLazy<int>(async () => | |
| { | |
| await Task.Delay(TimeSpan.FromSeconds(1)); // 模拟异步操作 | |
| return 13; | |
| }); |
调用代码:
| int value = await instance.Data; // 异步获取值 |
在这个实现中,AsyncLazy 确保异步计算只会执行一次,之后的访问直接返回缓存的结果。
反面示例和注意事项
在将同步属性改为异步时,务必避免如下的反面示例:
| private async Task<int> GetDataAsync() | |
| { | |
| await Task.Delay(TimeSpan.FromSeconds(1)); | |
| return 13; | |
| } | |
| // 错误:在属性中调用异步方法并阻塞 | |
| public int Data | |
| { | |
| get { return GetDataAsync().Result; } // 阻塞等待异步操作完成 | |
| } |
-
问题 1:性能和线程阻塞
使用.Result或.Wait()会阻塞当前线程,违背了异步编程的初衷,可能导致死锁或性能问题。 -
问题 2:语义不清晰
属性的语义应是快速访问数据,而不是启动复杂的操作。上述代码在 API 设计上容易误导调用方。
状态属性与异步语义
在将同步代码转换为异步代码时,还需要特别注意属性的状态语义问题。以流操作中的 Stream.Position 为例:
- 在同步方法
Stream.Read或Stream.Write中,Position会在操作完成后更新,反映当前的流位置。 - 在异步方法
Stream.ReadAsync或Stream.WriteAsync中,Position的更新时机可能会产生歧义:- 是在异步操作完成后更新?
- 还是在调用
ReadAsync或WriteAsync方法时立即更新?
这些语义问题在异步化过程中需要特别考虑,并且应在 API 文档中清晰说明。
完整代码示例
以下是一个完整的示例,展示了异步方法和异步延迟初始化的两种实现:
| using System; | |
| using System.Threading.Tasks; | |
| class MyClass | |
| { | |
| // 异步方法:每次调用都会重新计算值 | |
| public async Task<int> GetDataAsync() | |
| { | |
| await Task.Delay(TimeSpan.FromSeconds(1)); // 模拟异步操作 | |
| return 13; | |
| } | |
| // 异步延迟初始化:值只计算一次并缓存 | |
| public AsyncLazy<int> Data { get; } | |
| private readonly AsyncLazy<int> _data = new AsyncLazy<int>(async () => | |
| { | |
| await Task.Delay(TimeSpan.FromSeconds(1)); // 模拟异步操作 | |
| return 13; | |
| }); | |
| public MyClass() | |
| { | |
| Data = _data; | |
| } | |
| } | |
| class Program | |
| { | |
| static async Task Main(string[] args) | |
| { | |
| var myClass = new MyClass(); | |
| // 使用异步方法 | |
| int value1 = await myClass.GetDataAsync(); | |
| Console.WriteLine($"Value from GetDataAsync: {value1}"); | |
| // 使用异步延迟初始化 | |
| int value2 = await myClass.Data; | |
| Console.WriteLine($"Value from AsyncLazy<Data>: {value2}"); | |
| } | |
| } |
输出:
| Value from GetDataAsync: 13 | |
| Value from AsyncLazy<Data>: 13 |
通过这种方式,属性与方法的语义更加明确,既满足了异步计算的需求,又避免了设计上的歧义。
11.5 异步事件
问题
在设计异步事件时,如何跟踪事件处理程序的完成情况?通常情况下,事件的触发者不需要关心处理程序是否完成,这种情形多见于通知事件(如按钮点击)。但在某些情况下,触发者需要等待所有处理程序完成(如生命周期事件),这被称为命令事件。
一个常见的挑战是,async void 异步处理程序无法被直接跟踪,因为它不会返回 Task,因此我们需要一种替代方法来检测异步处理程序的完成状态。
解决方案:使用延迟管理器
为了解决这个问题,可以引入延迟管理器(DeferralManager),它可以跟踪事件处理程序的延迟状态:
- 处理程序分配延迟:延迟管理器为每个异步处理程序分配一个延迟对象,用于跟踪异步处理的状态。
- 延迟完成通知:当异步处理完成时,延迟对象会通知延迟管理器。
- 等待所有延迟完成:事件发送方可以等待延迟管理器跟踪的所有延迟完成后再继续执行。
实现步骤
1. 定义事件参数类型
为了支持延迟管理,事件参数类型需要扩展。可以通过实现 IDeferralSource 接口,并包含一个 DeferralManager 实例来管理延迟。
| public class MyEventArgs : EventArgs, IDeferralSource | |
| { | |
| private readonly DeferralManager _deferrals = new DeferralManager(); | |
| // 获取延迟对象 | |
| public IDisposable GetDeferral() | |
| { | |
| return _deferrals.DeferralSource.GetDeferral(); | |
| } | |
| // 等待所有异步延迟完成 | |
| internal Task WaitForDeferralsAsync() | |
| { | |
| return _deferrals.WaitForDeferralsAsync(); | |
| } | |
| } |
在 MyEventArgs 中:
GetDeferral方法用于分配延迟对象。WaitForDeferralsAsync方法用于等待所有延迟完成。
2. 触发异步事件
当触发事件时,需要等待所有异步处理程序完成。可以使用以下代码触发事件:
| public event EventHandler<MyEventArgs> MyEvent; | |
| private async Task RaiseMyEventAsync() | |
| { | |
| // 获取事件处理程序 | |
| EventHandler<MyEventArgs> handler = MyEvent; | |
| if (handler == null) | |
| return; | |
| // 创建事件参数 | |
| var args = new MyEventArgs(); | |
| // 触发事件 | |
| handler(this, args); | |
| // 等待所有异步处理程序完成 | |
| await args.WaitForDeferralsAsync(); | |
| } |
代码解读:
- 检查是否有订阅的处理程序。
- 创建
MyEventArgs实例。 - 调用事件处理程序。
- 调用
WaitForDeferralsAsync等待所有异步处理程序完成。
3. 异步事件处理程序
事件处理程序可以通过以下方式分配延迟,并在异步操作完成后通知延迟管理器:
| async void AsyncHandler(object sender, MyEventArgs args) | |
| { | |
| using IDisposable deferral = args.GetDeferral(); // 分配延迟 | |
| await Task.Delay(TimeSpan.FromSeconds(2)); // 模拟异步操作 | |
| } |
在这里,using 块确保延迟对象在异步操作完成后被正确释放,以此通知延迟管理器。
完整实现示例
以下是一个完整的代码示例,展示如何使用延迟管理器实现异步事件:
| using System; | |
| using System.Threading.Tasks; | |
| using Nito.AsyncEx; // 引入 Nito.AsyncEx 库 | |
| // 定义事件参数类型,支持延迟管理 | |
| public class MyEventArgs : EventArgs, IDeferralSource | |
| { | |
| private readonly DeferralManager _deferrals = new DeferralManager(); | |
| // 获取延迟对象 | |
| public IDisposable GetDeferral() | |
| { | |
| return _deferrals.DeferralSource.GetDeferral(); | |
| } | |
| // 等待所有异步延迟完成 | |
| internal Task WaitForDeferralsAsync() | |
| { | |
| return _deferrals.WaitForDeferralsAsync(); | |
| } | |
| } | |
| public class MyEventSource | |
| { | |
| // 定义事件 | |
| public event EventHandler<MyEventArgs> MyEvent; | |
| // 触发事件并等待异步处理程序完成 | |
| public async Task RaiseMyEventAsync() | |
| { | |
| EventHandler<MyEventArgs> handler = MyEvent; | |
| if (handler == null) | |
| return; | |
| // 创建事件参数 | |
| var args = new MyEventArgs(); | |
| // 触发事件 | |
| handler(this, args); | |
| // 等待所有异步处理程序完成 | |
| await args.WaitForDeferralsAsync(); | |
| } | |
| } | |
| public class Program | |
| { | |
| public static async Task Main(string[] args) | |
| { | |
| var source = new MyEventSource(); | |
| // 注册异步事件处理程序 | |
| source.MyEvent += async (sender, e) => | |
| { | |
| using IDisposable deferral = e.GetDeferral(); // 分配延迟 | |
| Console.WriteLine("Handler started."); | |
| await Task.Delay(TimeSpan.FromSeconds(2)); // 模拟异步操作 | |
| Console.WriteLine("Handler completed."); | |
| }; | |
| // 触发事件并等待处理程序完成 | |
| Console.WriteLine("Raising event..."); | |
| await source.RaiseMyEventAsync(); | |
| Console.WriteLine("Event processing completed."); | |
| } | |
| } |
输出:
| Raising event... | |
| Handler started. | |
| Handler completed. | |
| Event processing completed. |
通知事件 vs 命令事件
.NET 中的事件可以按语义分为两种:
-
通知事件:
- 用于通知订阅方某些情况发生。
- 通常是单向的,事件发送方并不关心订阅方是否完成处理。
- 特点:无需额外代码支持异步处理程序。例如,按钮单击事件属于通知事件。
- 处理方式:异步处理程序可以是
async void,发送方不需要等待其完成。
-
命令事件:
- 用于触发某些功能,发送方需要等待订阅方完成处理后才能继续。
- 特点:发送方需要检测订阅方的完成状态,异步处理程序需要显式等待。
- 处理方式:需要引入延迟机制(如
DeferralManager),以跟踪异步处理状态。
最佳实践
-
延迟的作用范围:
延迟机制主要针对命令事件,因为命令事件需要等待处理程序完成。而对于通知事件,这种机制是不必要的。 -
线程安全性:
事件参数的类型应该是线程安全的。最简单的实现方式是使事件参数不可变(所有属性均为只读)。 -
Nito.AsyncEx:
使用DeferralManager是一种简洁的扩展方式,可从 NuGet 包Nito.AsyncEx中直接获取。 -
异步事件的设计原则:
- 如果事件是通知性质的,无需额外代码支持异步处理程序。
- 如果事件是命令性质的,需要明确等待处理程序完成,且需要文档清晰说明其语义。
11.6 异步释放
问题
在某些类型中,需要在释放(Dispose)资源时处理异步操作。如何实现异步资源释放,同时确保语义清晰且与现有的 .NET 生态系统兼容?
解决方案
.NET 提供了两种常见模式来实现资源释放:
- 释放作为取消:将释放视为对所有正在进行操作的取消请求。
- 异步释放:使用异步语义,在释放资源时等待异步操作完成。
1. 将释放视为取消
这种模式适用于需要在释放资源时取消现有操作的场景,例如 HttpClient。通过使用 CancellationTokenSource 来取消当前操作,避免资源占用。
实现代码:
| class MyClass : IDisposable | |
| { | |
| private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource(); | |
| public async Task<int> CalculateValueAsync(CancellationToken cancellationToken = default) | |
| { | |
| using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _disposeCts.Token); | |
| await Task.Delay(TimeSpan.FromSeconds(2), combinedCts.Token); | |
| return 13; | |
| } | |
| public void Dispose() | |
| { | |
| _disposeCts.Cancel(); | |
| } | |
| } |
用法:
| async Task UseMyClassAsync() | |
| { | |
| Task<int> task; | |
| using (var resource = new MyClass()) | |
| { | |
| task = resource.CalculateValueAsync(); | |
| } | |
| // 在 Dispose 后调用 await 将抛出 OperationCanceledException | |
| var result = await task; | |
| } |
2. 异步释放(IAsyncDisposable)
异步释放在 C# 8.0 和 .NET Core 3.0 中引入,通过 IAsyncDisposable 接口和 DisposeAsync 方法实现。在释放资源时,可以等待异步操作完成。
实现代码:
| class MyClass : IAsyncDisposable | |
| { | |
| public async ValueTask DisposeAsync() | |
| { | |
| // 模拟异步释放操作 | |
| await Task.Delay(TimeSpan.FromSeconds(2)); | |
| } | |
| } |
用法:
使用 await using 语法来异步释放资源:
| await using (var myClass = new MyClass()) | |
| { | |
| // 使用资源 | |
| } | |
| // 此处调用并等待 DisposeAsync |
如果需要避免捕获同步上下文,可以使用 ConfigureAwait(false):
| var myClass = new MyClass(); | |
| await using (myClass.ConfigureAwait(false)) | |
| { | |
| // 使用 myClass 的逻辑 | |
| } | |
| // 此处调用 DisposeAsync 并避免上下文捕获 |
注意:DisposeAsync 返回 ValueTask 而非 Task。这可以减少内存分配成本,但两者都支持标准的 async/await 语法。
两种模式的比较
| 模式 | 优势 | 劣势 |
|---|---|---|
| 释放作为取消 | 简单且广泛兼容,适用于多数场景。 | 无法等待未完成的操作。 |
| 异步释放 | 支持异步操作完成,适用于需要严格释放的资源。 | 实现复杂,依赖 C# 8.0 及更高版本。 |
在某些场景下,两种模式可以结合使用:
- 如果客户端使用
Dispose,表示取消正在进行的操作。 - 如果客户端使用
DisposeAsync,则等待所有操作完成并释放资源。
组合实现代码:
| class MyClass : IDisposable, IAsyncDisposable | |
| { | |
| private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource(); | |
| public async Task<int> CalculateValueAsync(CancellationToken cancellationToken = default) | |
| { | |
| using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _disposeCts.Token); | |
| await Task.Delay(TimeSpan.FromSeconds(2), combinedCts.Token); | |
| return 13; | |
| } | |
| public void Dispose() | |
| { | |
| _disposeCts.Cancel(); // 取消操作 | |
| } | |
| public async ValueTask DisposeAsync() | |
| { | |
| _disposeCts.Cancel(); // 取消操作 | |
| await Task.Delay(TimeSpan.FromSeconds(2)); // 模拟异步释放资源 | |
| } | |
| } |
浙公网安备 33010602011771号