Course contentsShow
← C#
Lesson 1733 of 1,90816. Performance and DiagnosticsFree lesson

Implementing the IDisposable Pattern Correctly

Learners will create reliable cleanup routines for unmanaged resources to prevent memory leaks.

In this lesson, you will learn how to implement the IDisposable pattern to ensure that unmanaged resources are released deterministically, preventing memory leaks and system instability.

Why Explicit Cleanup Matters

While the Garbage Collector (GC) excels at reclaiming memory for managed objects, it is blind to "unmanaged" resources like file handles, database connections, or socket pointers. If you don't release these, your application will eventually exhaust system resources.

The IDisposable pattern provides a standardized way to signal that an object is finished with its unmanaged assets.

The Correct Implementation Pattern

To handle this safely, we use the "Dispose Pattern," which separates the cleanup logic into a protected method. This ensures we can clean up whether the user calls Dispose() manually or if the GC eventually finalizes the object.

public class ResourceHandler : IDisposable
{
    private bool _disposed = false;
    private IntPtr _nativeResource = /* allocate unmanaged resource */;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // Tells GC not to run the finalizer
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;

        if (disposing)
        {
            // Free managed state (e.g., other IDisposable objects)
        }

        // Free unmanaged resources here
        ReleaseNativeResource(_nativeResource);
        
        _disposed = true;
    }

    ~ResourceHandler() => Dispose(false); // Finalizer for safety
}

Key Considerations

  1. The disposing Flag: When true, we clean up both managed and unmanaged resources. When false (called from the finalizer), we only touch unmanaged resources, as managed objects might have already been collected by the GC.
  2. GC.SuppressFinalize(this): This is critical. Once a user has manually called Dispose(), the object is cleaned up. We call this to prevent the GC from wasting resources running the finalizer later.
  3. Idempotency: Always ensure your Dispose logic can be called multiple times without throwing exceptions. The _disposed flag prevents "double-disposal" errors.

By following this structure, you ensure your components are good citizens in the .NET ecosystem, releasing expensive resources the moment they are no longer needed.

Key Takeaway: Always implement the full Dispose pattern to handle both managed and unmanaged cleanup safely, and use GC.SuppressFinalize to optimize performance once cleanup is done.