DEV Community

Libin Tom Baby
Libin Tom Baby

Posted on

IDisposable, Finalizers, and the Dispose Pattern — The Complete Guide for .NET Developers


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();
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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
  • IDisposable enables deterministic cleanup
  • Finalizers are a safety net, not the primary cleanup mechanism
  • The Dispose Pattern ensures safe cleanup in all cases
  • Use using or await using to guarantee disposal
  • Prefer SafeHandle over 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)