IDisposable and the Dispose pattern

2 min read

Purpose

The GC handles managed memory. But applications often use unmanaged resources such as file handles, database connections, network sockets, GDI handles, or pointers to unmanaged memory, and the GC does not know how to release them. The IDisposable interface gives objects a contract for offering a deterministic way to release such resources. It is also crucial for managing the lifetime of managed objects that themselves wrap disposable resources, so that cleanup cascades.

The interface

IDisposable is defined in the System namespace and has a single public method: void Dispose();.

The using statement

The C# using statement is a convenient syntax that ensures Dispose() is called on an IDisposable object when it goes out of scope, even if an exception occurs inside the using block. This is the recommended way to handle disposable objects.

This is a conceptual example. Both using lines are on lines 1 and 2, and the comment on line 5 says what happens at the closing brace.

C#
using (var fileStream = new FileStream("path/to/file.txt", FileMode.Open))using (var streamReader = new StreamReader(fileStream)){    // Use streamReader to read from the file} // streamReader.Dispose() and fileStream.Dispose() are called automatically here.

Implementing the Dispose pattern

For a class that directly owns unmanaged resources or managed disposable resources, a robust implementation of the Dispose pattern typically involves:

  1. Implementing IDisposable.
  2. A public Dispose() method. It should call a virtual Dispose(bool disposing) method with disposing set to true, and then call GC.SuppressFinalize(this). That stops the GC from calling the object’s finalizer (if one exists), since the cleanup has already been done.
  3. A protected virtual Dispose(bool disposing) method that contains the actual cleanup logic. It should check whether the resources have already been disposed, to make it idempotent.

Inside Dispose(bool disposing):

  • If disposing is true, the call came from Dispose() and not from the finalizer. Release both the managed resources (by calling Dispose() on them) and the unmanaged resources.
  • If disposing is false, the call came from the finalizerFinalizersA fail-safe for when Dispose was never called, and why you should rarely write one.Opens in a new tab. Release only the unmanaged resources. Don’t touch managed resources here, because their state might be indeterminate during finalization.

Idempotency

Dispose() and Dispose(bool) should be safe to call multiple times without throwing exceptions. Later calls should do nothing.


Sources