乍看之下,.NET 中的处置(Disposal)似乎很简单:将资源包裹在 using 块中,让运行时以确定性方式清理它。但一旦你超越了基础流(streams)和内存句柄(memory handles),你就会发现处置是一个充满陷阱的战场。异步资源、依赖注入、非托管句柄以及同步/异步混合清理,即使对经验丰富的开发者也处处是坑。
处置的艺术:精通 C# 中的 IDisposable 正确的异步清理:为何 IAsyncDisposable 在现代 .NET 中至关重要 本文将深入探讨高级处置模式,这些模式将初级的 .Dispose() 知识与专业级的清理规范区分开来。
高级概念 作为一名高级工程师,你应该了解以下一些高级处置概念:
同步与异步处置 并非所有资源都需要异步清理。例如:
可以将同步处置视为快速的“立即释放此指针”,而异步处置则是“请等待我刷新并通知外部系统后再离开”。
完整处置模式(无终结器) 如果你的类型拥有可处置资源,你应该实现完整的处置模式。对于大多数现代应用程序(尤其是不鼓励使用终结器的场景),这意味着提供一个安全、可重复的清理机制:
public class ResourceHolder : IDisposable
{
private bool _disposed;
private readonly Stream _stream;
public ResourceHolder(Stream stream) => _stream = stream;
public void Dispose()
{
if (_disposed) return;
_stream.Dispose(); // 释放拥有的资源
_disposed = true;
}
}
注意其幂等性:多次调用 Dispose 不会破坏对象。
GC.SuppressFinalize
你现在很少需要终结器了,但如果你确实将终结器与 Dispose 结合使用,请务必调用 GC.SuppressFinalize(this) 以防止在手动清理后运行终结器:
public void Dispose()
{
// 释放资源...
GC.SuppressFinalize(this);
}
在现代 .NET 中,终结器主要用于非托管内存包装器。如果你不直接拥有非托管资源,可以安全地避免使用它们。
处置与依赖注入 (ASP.NET Core) 在 ASP.NET Core 中,DI 容器负责管理已注册的可处置对象。这意味着:
这使得不要自行处置由 DI 管理的服务变得至关重要。始终让容器来处理它。
错误与正确方法对比 以下是一些正确/错误方法的示例:
错误:在 Dispose() 内部进行异步清理
public void Dispose()
{
// 不要这样做:会阻塞线程池线程
SomeAsyncCleanup().GetAwaiter().GetResult();
}
这会强制在同步上下文中运行异步代码,在 ASP.NET 或 GUI 应用程序中可能导致死锁。
正确:提供 IAsyncDisposable 进行异步清理
public async ValueTask DisposeAsync()
{
await SomeAsyncCleanup();
}
错误:糟糕地混合使用 IDisposable 和 IAsyncDisposable
public class Hybrid : IDisposable, IAsyncDisposable
{
public void Dispose()
{
// 同步清理
}
public ValueTask DisposeAsync()
{
Dispose(); // 并未实际释放异步资源
return ValueTask.CompletedTask;
}
}
正确:分别并谨慎地处理两者
public class Hybrid : IDisposable, IAsyncDisposable
{
private readonly Stream _stream;
private readonly HttpClient _client;
public Hybrid(Stream stream, HttpClient client)
{
_stream = stream;
_client = client;
}
public void Dispose() => _stream.Dispose();
public async ValueTask DisposeAsync()
{
_stream.Dispose();
_client.Dispose(); // HttpClient 是同步的
// 如果有异步资源,在这里 await 它们
await Task.CompletedTask;
}
}
库作者的检查清单 编写可重用库时,请问自己:
using 或 await using 中。常见错误 / 反模式
Dispose 或 DisposeAsync。using 就足够的情况下实现完整的处置模式。视觉概述
处置不仅仅是为了释放内存;它关乎对稀缺资源进行确定性清理。随着 .NET 拥抱异步优先的 API,IAsyncDisposable 对于防止泄漏和死锁变得愈发关键。
无论你是编写应用程序代码还是库,遵循这些高级处置模式都能确保你的软件健壮、高效且为生产环境做好准备。