A Brief Discussion on the Dispose Pattern and the using Statement
TLDR
- Unmanaged resources (such as database connections and files) must be manually released by implementing the
IDisposableinterface or defining a finalizer. - When implementing the Dispose Pattern, prioritize the
Dispose()method and useGC.SuppressFinalize(this)to prevent the garbage collector from calling the finalizer repeatedly. IAsyncDisposableis used for asynchronous resource release; it is recommended to implementIDisposablesimultaneously to maintain backward compatibility.- The
usingstatement is essentially syntactic sugar fortry...finally, ensuring that an object correctly callsDispose()when it goes out of scope. - The
usingdeclaration syntax introduced in C# 8.0 reduces nested indentation and improves code readability.
Dispose Pattern
Releasing Unmanaged Resources
When does this issue arise: When developers use unmanaged resources such as database connections or file access, the CLR garbage collector (GC) cannot manage these resources automatically, so they must be released manually.
There are two main ways to release unmanaged resources:
- Implement the
IDisposableinterface: Manually release resources in theDispose()method. - Declare a finalizer: Automatically called when the GC collects the object, though the
Dispose()method should be prioritized.
Implementation Example
When implementing the Dispose Pattern, it is recommended to use the following standard pattern to ensure that resources are released correctly and not executed repeatedly:
public class ResourceHandle : IDisposable {
private bool disposed = false;
private IntPtr unmanagedResource;
private ManagedResource managedResource;
public ResourceHandle() {
unmanagedResource = IntPtr.Zero;
managedResource = new ManagedResource();
}
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) {
if (!disposed) {
if (disposing) {
managedResource?.Dispose();
managedResource = null;
}
if (unmanagedResource != IntPtr.Zero) {
FreeUnmanagedResource(unmanagedResource);
unmanagedResource = IntPtr.Zero;
}
disposed = true;
}
}
~ResourceHandle() {
Dispose(false);
}
}Asynchronous Dispose Pattern
When does this issue arise: When the resource release process involves asynchronous operations (such as network requests), the IAsyncDisposable interface introduced in .NET Core 3.0 should be used.
- Best Practice Recommendation: Implement both
IDisposableandIAsyncDisposableto ensure that resources can still be handled correctly in older frameworks that do not support asynchronous release. - Priority: In the ASP.NET Core dependency injection (DI) container, if an object implements both,
DisposeAsync()will be called first.
class ExampleConjunctiveDisposable : IDisposable, IAsyncDisposable {
IDisposable? disposableResource = new MemoryStream();
IAsyncDisposable? asyncDisposableResource = new MemoryStream();
public void Dispose() {
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
public async ValueTask DisposeAsync() {
await DisposeCoreAsync().ConfigureAwait(false);
Dispose(disposing: false);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) {
if (disposing) {
disposableResource?.Dispose();
disposableResource = null;
if (asyncDisposableResource is IDisposable disposable) {
disposable.Dispose();
asyncDisposableResource = null;
}
}
}
protected virtual async ValueTask DisposeCoreAsync() {
if (asyncDisposableResource is not null) {
await asyncDisposableResource.DisposeAsync().ConfigureAwait(false);
}
if (disposableResource is IAsyncDisposable disposable) {
await disposable.DisposeAsync().ConfigureAwait(false);
} else {
disposableResource?.Dispose();
}
asyncDisposableResource = null;
disposableResource = null;
}
}using Statement
Ensuring Resource Release
When does this issue arise: To avoid resource leaks caused by forgetting to call Dispose(), or when cleanup logic cannot be executed due to an exception.
The compiler automatically converts the using statement into a try...finally structure, ensuring that resources are released regardless of whether an exception occurs.
// Traditional syntax
using (ResourceHandle handle = new ResourceHandle()) {
// Execution logic
}
// C# 8.0 new syntax: Automatically calls Dispose() when leaving scope
{
using ResourceHandle handle = new ResourceHandle();
// Execution logic
}Nesting and Asynchronous Processing
For the release of multiple resources, nested structures can be simplified:
- Combined Declaration: If the types are the same, they can be combined into a single
using. - Asynchronous Release: If the object implements
IAsyncDisposable,await usingmust be used.
// Combined declaration
using (ResourceHandle handle1 = new ResourceHandle(), handle2 = new ResourceHandle()) {
// Execution logic
}
// Asynchronous release
await using (AsyncDisposableObject resource = new AsyncDisposableObject()) {
// Execution logic
}Change Log
- 2024-08-08 Initial document creation.