The other day I got a code review comment saying that I should implement the full blown disposable pattern for IDisposable including the
protected virtual void Dispose(bool disposing) method. For a long time I had implemented the IDisposable without caring about the additional overload - and I started worrying how many bugs I've introduced by not doing so.
The most recent official documentation I could find describes the pattern in details, which I'll summarize below.
- Implement the IDisposable interface and its Dispose method
- Add a
protected virtual void Dispose(bool disposing)overload for the Dispose method
- Make the IDisposable.Dispose implementation call Dispose(disposing: true)
- Add a Finalizer/Destructor for your class that calls Dispose(disposing" false)
- In the Dispose(bool disposing), if disposing is false that means your object is being destructed in the process of garbage collection, references to other managed objects may not be relied upon anymore because they may have been collected already; on the other hand, with disposing == true, you can still rely on managed references
Note that the documentation also talks about a variant of what I described that involves wrapping unmanaged references with a SafeHandle, which I am skipping over, and you can read more in the official docs.
The documentation makes it very clear that you must not rely on managed code depending from where the call to Dispose comes from:
If the method call comes from a finalizer (that is, if disposing is false), only the code that frees unmanaged
resources executes. Because the order in which the garbage collector destroys managed objects during
finalization is not defined, calling this Dispose overload with a value of false prevents the finalizer from
trying to release managed resources that may have already been reclaimed.
However, I did prefer the explanation in this older documentation
// Dispose(bool disposing) executes in two distinct scenarios.
// If disposing equals true, the method has been called directly // or indirectly by a user's code. Managed and unmanaged resources // can be disposed. // If disposing equals false, the method has been called by the // runtime from inside the finalizer and you should not reference // other objects. Only unmanaged resources can be disposed.
Do I really need all this?
The short answer is if you don't directly hold unmanaged resources, you don't need to worry about adding a finalizer and the overloaded Dispose method with the boolean parameter; just implement the IDisposable.Dispose and dispose of your managed references (usually by calling Dispose() on them). Now, if you do directly reference unmanaged resources, then you really should do it as the docs say.
This is so because if you only hold references to other managed objects (that hold themselves the unmanaged resources) and your object ends up being garbage collected, your managed references should be collected (and destroyed), which will trigger the finalizer on those that should take care or releasing unmanaged resources, as described in the previous section.
But what if you are referencing poorly implemented objects that will not implement the Disposable pattern with the finalizer? Well, you can't run away from it, implementing the pattern with the finalizer on your class is not an option, because when the finalizer kicks in, your reference to the buggy object may not exist anymore. Your options are either fix that class itself, if you own that code; or, if that is not your code, filling a bug to the project that owns it.
But why do I need to bother with the Finalizer at all?
You shouldn't ideally. No disposable object should leave scope without being disposed. There is a code analysis rule that enforces that. But no one is perfect, and that may happen. A special case to keep an eye out for disposable objects being used as Singletons or in static variables - that's because rarely people bother with listening for the event of the process (or AppDomain) being unloaded and those references are never disposed. In such cases, the Finalizer ends up being the last rescue for getting the unmanaged resources freed. Plus, Singletons are a bad idea anyways.
However, in reality, mistakes happen. If you are writing a library, especially, you should take extra care to make sure your Disposable pattern is implemented correctly with the finalizer (if you use unmanaged resources directly, that is) - that will give you the last chance to free the unmanaged resources if your callers forgot to dispose your object.