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)
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
Key characteristics:
- Called explicitly by the developer
- Runs at a known, predictable time
- Can clean up both managed and unmanaged resources
- Works with the
usingstatement /usingdeclaration - No GC overhead
The Dispose Pattern (Combining Both)
The standard pattern implements both as a safety net:
Dispose(true)— called fromDispose(), cleans up everythingDispose(false)— called from finalizer, only cleans up unmanaged resourcesGC.SuppressFinalize(this)— tells the GC not to run the finalizer since cleanup is done
Using Statement
The using statement guarantees Dispose() is called even if an exception occurs — it compiles to a try-finally block.
When to Use Each
| Scenario | Use |
| File handles, streams | IDisposable with using |
| Database connections | IDisposable with using |
| Network sockets | IDisposable with using |
| Unmanaged memory (IntPtr) | IDisposable + finalizer safety net |
| Event handler unsubscription | IDisposable |
| Pure managed objects | Neither (let GC handle it) |
| Third-party native library wrappers | Full Dispose pattern with finalizer |
IAsyncDisposable (C# 8+)
For async cleanup operations:
GC Performance Impact
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
IDisposablefor deterministic cleanup. Use the destructor only as a fallback. - Forgetting
GC.SuppressFinalize: Without this, the finalizer runs even afterDispose()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 usingusing: Creating anIDisposableobject withoutusingor manualDispose()leaks the resource until the GC eventually runs the finalizer (if one exists). - Disposing objects you do not own: If a method receives an
IDisposableas 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
IDisposablewithusingfor all resource cleanup - Add a destructor only as a safety net for unmanaged resources
- Always call
GC.SuppressFinalize(this)inDispose()to avoid double cleanup - The full Dispose pattern handles both managed (
disposing: true) and unmanaged resources

