Destructor
IDisposable
C#
.NET
garbage collection

Destructor vs IDisposable?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

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

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.