Destructor
IDisposable
C#
.NET
garbage collection

Destructor vs IDisposable?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In C#, destructors (finalizers) and IDisposable both handle resource cleanup, but they work differently. IDisposable.Dispose() is called explicitly by your code (often via using) and runs deterministically — you know exactly when cleanup happens. A destructor (~ClassName) is called by the garbage collector at an unpredictable time. For unmanaged resources (file handles, database connections, network sockets), always implement IDisposable. Use a destructor only as a safety net in case Dispose() was never called.

Destructor (Finalizer)

csharp
1class FileWrapper
2{
3    private IntPtr fileHandle;
4
5    ~FileWrapper()
6    {
7        // Called by GC — timing is unpredictable
8        CloseHandle(fileHandle);
9        Console.WriteLine("Finalizer called");
10    }
11}

Key characteristics:

  • Called by the garbage collector, not by your code
  • Cannot predict when (or if) it will run
  • Adds GC overhead — objects with finalizers take two GC cycles to collect
  • Cannot access other managed objects (they may already be collected)
  • One finalizer per class, no parameters, no access modifiers

IDisposable

csharp
1class FileWrapper : IDisposable
2{
3    private IntPtr fileHandle;
4    private bool disposed = false;
5
6    public void Dispose()
7    {
8        // Called explicitly — runs immediately
9        if (!disposed)
10        {
11            CloseHandle(fileHandle);
12            disposed = true;
13            Console.WriteLine("Dispose called");
14        }
15    }
16}
17
18// Usage with 'using' statement
19using (var file = new FileWrapper())
20{
21    // Work with the file
22} // Dispose() called here, immediately and reliably

Key characteristics:

  • Called explicitly by the developer
  • Runs at a known, predictable time
  • Can clean up both managed and unmanaged resources
  • Works with the using statement / using declaration
  • No GC overhead

The Dispose Pattern (Combining Both)

The standard pattern implements both as a safety net:

csharp
1class DatabaseConnection : IDisposable
2{
3    private IntPtr connectionHandle;  // Unmanaged resource
4    private Stream logStream;          // Managed resource
5    private bool disposed = false;
6
7    public void DoWork()
8    {
9        ObjectDisposedException.ThrowIf(disposed, this);
10        // Use resources...
11    }
12
13    public void Dispose()
14    {
15        Dispose(disposing: true);
16        GC.SuppressFinalize(this);  // Prevent finalizer from running
17    }
18
19    protected virtual void Dispose(bool disposing)
20    {
21        if (!disposed)
22        {
23            if (disposing)
24            {
25                // Clean up managed resources
26                logStream?.Dispose();
27            }
28
29            // Clean up unmanaged resources (always)
30            CloseConnection(connectionHandle);
31            connectionHandle = IntPtr.Zero;
32
33            disposed = true;
34        }
35    }
36
37    ~DatabaseConnection()
38    {
39        // Safety net: clean up if Dispose() was never called
40        Dispose(disposing: false);
41    }
42}
  • Dispose(true) — called from Dispose(), cleans up everything
  • Dispose(false) — called from finalizer, only cleans up unmanaged resources
  • GC.SuppressFinalize(this) — tells the GC not to run the finalizer since cleanup is done

Using Statement

csharp
1// using statement (block scope)
2using (var conn = new DatabaseConnection())
3{
4    conn.DoWork();
5} // Dispose() called automatically here
6
7// using declaration (C# 8+, scope of enclosing block)
8using var conn = new DatabaseConnection();
9conn.DoWork();
10// Dispose() called at end of enclosing method/block

The using statement guarantees Dispose() is called even if an exception occurs — it compiles to a try-finally block.

When to Use Each

ScenarioUse
File handles, streamsIDisposable with using
Database connectionsIDisposable with using
Network socketsIDisposable with using
Unmanaged memory (IntPtr)IDisposable + finalizer safety net
Event handler unsubscriptionIDisposable
Pure managed objectsNeither (let GC handle it)
Third-party native library wrappersFull Dispose pattern with finalizer

IAsyncDisposable (C# 8+)

For async cleanup operations:

csharp
1class AsyncDbConnection : IAsyncDisposable
2{
3    private DbConnection connection;
4
5    public async ValueTask DisposeAsync()
6    {
7        if (connection != null)
8        {
9            await connection.CloseAsync();
10            connection = null;
11        }
12    }
13}
14
15// Usage
16await using var conn = new AsyncDbConnection();
17await conn.QueryAsync("SELECT 1");
18// DisposeAsync() called automatically

GC Performance Impact

csharp
1// Objects with finalizers are MORE expensive to collect:
2// 1. GC finds the object is unreachable
3// 2. Object is placed on the finalization queue (survives this GC)
4// 3. Finalizer thread runs ~ClassName()
5// 4. Object is collected in the NEXT GC cycle
6
7// GC.SuppressFinalize removes this overhead:
8public void Dispose()
9{
10    CleanUp();
11    GC.SuppressFinalize(this);  // Skip the finalization queue
12}

Objects with finalizers always survive to Gen 1 or Gen 2, making them much more expensive to collect. Always call GC.SuppressFinalize in Dispose().

Common Pitfalls

  • Only implementing a destructor: Destructors run at unpredictable times and add GC overhead. Always implement IDisposable for deterministic cleanup. Use the destructor only as a fallback.
  • Forgetting GC.SuppressFinalize: Without this, the finalizer runs even after Dispose() has already cleaned up, wasting time and potentially causing errors from double cleanup.
  • Accessing managed objects in the finalizer: When the finalizer runs, other managed objects may already be garbage collected. Only clean up unmanaged resources in the finalizer (Dispose(false) path).
  • Not calling Dispose() / not using using: Creating an IDisposable object without using or manual Dispose() leaks the resource until the GC eventually runs the finalizer (if one exists).
  • Disposing objects you do not own: If a method receives an IDisposable as a parameter, do not dispose it unless the method is documented as taking ownership. The caller may still need it.

Summary

  • IDisposable.Dispose() is deterministic — called explicitly, runs immediately
  • Destructors are non-deterministic — called by GC at an unpredictable time
  • Use IDisposable with using for all resource cleanup
  • Add a destructor only as a safety net for unmanaged resources
  • Always call GC.SuppressFinalize(this) in Dispose() to avoid double cleanup
  • The full Dispose pattern handles both managed (disposing: true) and unmanaged resources

Course illustration
Course illustration

All Rights Reserved.