IDisposable, Finalizers, and the Dispose Pattern in .NET
Memory management in .NET is automatic thanks to the Garbage Collector (GC). But GC only handles managed memory. When your application interacts with unmanaged resources — file handles, database connections, sockets, streams, OS handles — the GC cannot clean them up.
That’s where IDisposable, finalizers, and the Dispose Pattern come in.
This guide explains how they work, why they exist, and how to implement them correctly in real-world .NET applications.
Why Do We Need IDisposable?
GC cleans up managed objects, but unmanaged resources live outside the .NET runtime.
Examples of unmanaged resources:
- File handles
- Network sockets
- Database connections
- OS handles
- Native memory
- GDI+ objects
These must be released deterministically, not “whenever GC runs.”
IDisposable gives you a way to clean up these resources immediately.
What Is IDisposable?
IDisposable defines a single method:
public interface IDisposable
{
void Dispose();
}
Calling Dispose() releases unmanaged resources.
The using statement ensures cleanup
using (var stream = new FileStream("data.txt", FileMode.Open))
{
// work with stream
} // Dispose() is called automatically here
This is deterministic cleanup — the resource is released immediately.
What Are Finalizers?
A finalizer (also called a destructor) is a method that runs when the GC collects an object.
~MyClass()
{
// cleanup logic
}
But finalizers are expensive
- They delay garbage collection
- They require extra GC passes
- They keep objects alive longer
- They run on a background thread
- Execution timing is unpredictable
Finalizers should be used only as a safety net, not as the primary cleanup mechanism.
The Dispose Pattern (The Correct Way)
When your class holds unmanaged resources, implement the full Dispose Pattern.
Standard implementation
public class ResourceHolder : IDisposable
{
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// Dispose managed resources
}
// Free unmanaged resources
_disposed = true;
}
~ResourceHolder()
{
Dispose(false);
}
}
Why this pattern?
-
Dispose(true)→ clean managed + unmanaged resources -
Dispose(false)→ clean unmanaged only (finalizer path) -
GC.SuppressFinalize→ prevents finalizer from running unnecessarily
This ensures safe cleanup in all scenarios.
When Should You Implement IDisposable?
Implement IDisposable when your class:
- Directly holds unmanaged resources
- Wraps a type that implements
IDisposable - Uses OS handles
- Uses native memory
- Uses streams, DB connections, HttpClient, etc.
Example: wrapping a disposable object
public class ReportWriter : IDisposable
{
private readonly StreamWriter _writer;
public ReportWriter(string path)
{
_writer = new StreamWriter(path);
}
public void Dispose()
{
_writer.Dispose();
}
}
Common Mistakes
❌ Forgetting to call Dispose
Leads to file locks, memory leaks, socket exhaustion.
❌ Using finalizers unnecessarily
Hurts performance.
❌ Using async void with disposable resources
Exceptions become uncatchable.
❌ Disposing objects you don’t own
Only dispose what your class created.
SafeHandle — The Modern Way
Instead of writing finalizers manually, .NET recommends using SafeHandle.
public class MyResource : IDisposable
{
private SafeHandle _handle = new SafeFileHandle(IntPtr.Zero, true);
public void Dispose()
{
_handle.Dispose();
}
}
Benefits
- No need for finalizers
- Built-in reliability
- Better performance
- Less boilerplate
Async Dispose (IAsyncDisposable)
For async cleanup:
public class MyAsyncResource : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await stream.DisposeAsync();
}
}
Used for:
- Asynchronous streams
- Network operations
- Database connections
Real‑World Scenarios
Scenario 1: File I/O
File handles must be released immediately.
Scenario 2: Database connections
Connection pools get exhausted if Dispose is not called.
Scenario 3: Network sockets
Unreleased sockets cause port exhaustion.
Scenario 4: Interop with native libraries
Unmanaged memory must be freed manually.
Interview‑Ready Summary
- GC cleans managed memory, not unmanaged resources
-
IDisposableenables deterministic cleanup - Finalizers are a safety net, not the primary cleanup mechanism
- The Dispose Pattern ensures safe cleanup in all cases
- Use
usingorawait usingto guarantee disposal - Prefer
SafeHandleover manual finalizers
A strong interview answer:
“IDisposable exists because GC cannot clean unmanaged resources. Dispose provides deterministic cleanup, while finalizers act as a fallback. The Dispose Pattern ensures both managed and unmanaged resources are released safely.”
Top comments (0)