第十一章: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)); // 模拟异步释放资源
}
}