In .NET, the relationship between Dispose() and finalizers is confusing. In this post, we take a look at the Dispose pattern, the snowball effect of the pattern and why the pattern is broken. Then, we take a look at one of the solutions, namely, SafeHandles. Finally, we reanalyze the problem.
Introduction
Since .NET appeared, one of the most confusing things was the relationship between Dispose()
(from the IDisposable
interface) and finalizers.
Original Idea
The original idea was very simple: The Garbage Collector does the memory and resource cleanup for us, so we don't need to manage memory and resources manually.
In practice, things got much more complicated. The Garbage Collector might not run when we need it to run (as it is said, it is "non-deterministic"). Also, static
fields keep objects alive and our code can interact with native code, and we need more control on objects and memory lifetime.
This justified having finalizers to release external resources when the Garbage Collector ran, and also Dispose()
methods to release any resource "as soon as possible" without waiting for the Garbage Collector to run. And that created a snowball effect, where we change entire class hierarchies to implement a hard pattern, as usually we want to have control when resources are freed (the Dispose()
method and the IDisposable
interface) and also a finalizer, as any "unmanaged" data needs to be released even if we (or users of our code) don't invoke Dispose()
.
The Original Solution - Dispose Pattern
The original solution is the Dispose pattern. It is not really easy, as it consists of:
- having a finalizer (calling
Dispose(false);
); - having a
Dispose()
(calling Dispose(true);
and GC.SuppressFinalize(this);
); - having an overloaded
Dispose(bool disposing);
which decided what to really do according to disposing
. This overload could be virtual or not, increasing the complexity of the pattern.
The "Snowball" Effect of the Dispose Pattern
Having to decide if an object needs to be disposable or not is, in itself, problematic. And the Dispose pattern interacts really badly with base-classes and frameworks.
If a framework object might ever need "predictable" destruction, that means we need a Dispose()
or similar method. But, as a framework or base class, that also means objects from sub-classes might possibly have unmanaged data, meaning we "need" the finalizer.
Now, the entire pattern needs to be used on any base class that can possibly have a sub-class dealing with unmanaged data.
So, instead of having something as simple as:
public abstract class MyBaseClass:
IDisposable
{
public virtual void Dispose()
{
}
}
We need to have something like the following instead:
public abstract class MyBaseClass:
IDisposable
{
~MyBaseClass()
{
Dispose(false);
}
public void Dispose()
{
GC.SupressFinalize(this);
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
}
}
It is important to notice that in the first block of code, Dispose()
was virtual
. In the second, Dispose()
isn't supposed be virtual
and, instead, Dispose(bool disposing)
needs to be virtual
... and also not-public, as it is not supposed to be invoked by user code.
What is "disposing"?
When I first saw Dispose()
with a "disposing" parameter, I got really confused. What is "disposing" in a method already named "Dispose
"?
I really think that if the Dispose pattern was named Release pattern and we had an "isFromManualDispose
" argument, things would be less confusing. It would still be a problematic pattern, but it would be a little easier to understand what the argument means.
Why Is the Pattern Broken?
"Broken" is too strong, to be honest, but I wanted something that grabbed the reader's attention. It is bad because it depends on too many methods and concepts, and it's also confusing. Even if "it works" when implemented correctly, it:
- is hard for new developers;
- means any framework class needs to have a
Dispose(bool)
to deal with possibly "unsafe" data even when the entire framework doesn't use any unsafe code; - means that anybody inheriting those classes needs to know what to do with that "hateful"
disposing
argument; - means it breaks the Single Responsibility Principle. A framework class (or just any base class) is not supposed to deal with all of that "just in case" a sub-class needs it.
The Solution: SafeHandles
Sometime later, Microsoft noticed the bad pattern and tried to fix it. That's when we got SafeHandle
s.
The entire idea of having SafeHandle
s is that, instead of having our own classes dealing with "managed and unmanaged memory", our classes should just deal with managed memory or, if really needed, deal with a "safe handle", which is the object that will have a finalizer and will really manage the lifetime of the unmanaged data.
I really think Microsoft documented it right in the beginning, but when I tried to find their good example, I just found the latest document, which is completely "busted". They explain why SafeHandle
s are good and help us avoid the bad Disposable pattern but then show a class that uses a SafeHandle
and also implements the Dispose pattern for no real benefit!
After reading the document, it seems that now we have a new pattern on top of the hard pattern. But that is just plain wrong. The new pattern came to replace the old, and hard, pattern. Not to add to it.
For those who are curious, I am referring to this page.
In that page, on the source code, there's even this comment:
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
protected virtual void Dispose(bool disposing)
- The comment about a subclass introducing a finalizer is another issue with the Dispose pattern. Some people argue that we shouldn't add a finalizer until we need it, but then if the base class ever decides to use a finalizer, we might have double-disposes happening. We should avoid all that confusion;
- The word "Howerver" is misspelled in that sample. Editors, please don't fix it here in the article;
- They say "Dispose is properly implemented here", but they actually forgot to to check the
disposing
argument. They were not supposed to call _handle.Dispose()
when disposing
is false.
The "new" Pattern
SafeHandle
s were created with the purpose of simplifying the pattern. When using SafeHandle
s, we should not worry about the entire Dispose pattern. Instead, we should just need to know if we are implementing Dispose()
or not. Just the simple Dispose()
, not that weird Dispose(bool disposing)
.
Then, if we are dealing with Windows Handles, we use the appropriate safe-handles and, if an object is never disposed and gets Garbage Collected, the SafeHandle
s do the work for us.
This, actually, is the base for a new and improved pattern, even when we don't have SafeHandle
s for the kind of unmanaged memory or data we are dealing with.
What Exactly Is the New Pattern When Not Using SafeHandles?
Simple rules:
Public
classes should not have a destructor or a Dispose(bool)
. If they are disposable, they just implement IDisposable
with the standard Dispose()
; - If they use any data that might need a destructor, they should use a helper class to hold that data. That's exactly what
SafeHandle
s are: Helper classes that hold the data and deal with the destructor for you (and nothing else).
In a way, that's all.
How to Implement One of the Helper Classes?
The helper classes will need to have a destructor, and possibly a Dispose()
to allow for an early release of the resources. But those helper classes can possibly be sealed and avoid any logic to deal with managed + unmanaged data. They exist with the sole purpose of dealing with the release of unmanaged data, so there's no need to check for that. They should not do anything else, as that would be the work of the main class. They are just simple helpers.
Reanalyzing the Problem
-
Dispose pattern:
Class with Dispose(bool)
, a finalizer that calls Dispose(false)
, an overloaded Dispose()
that calls Dispose(true)
, and that bool disposing
makes many developers wonder what is really going on, even if their classes never use unmanaged data.
-
New pattern:
Just a simple implementation of the IDisposable
interface if the class needs deterministic cleanup, which should be virtual
if the class can be inherited. If any unmanaged data is used by the class, a helper class (which can be the same for all unmanaged data, like a SafeHandle
) is used. That's all. No "disposing
" argument and no odd implementations because of such an argument.
By default, base classes and sub-classes will be much simpler when they don't hold unmanaged data. They will still be able to hold unmanaged data if needed, but will delegate the "release" of that data to a helper class.
History
- 30th April, 2020: Changed the title to include the bool(disposing). Introduced a comment that "Howerver" is misspelled in the original document;
- 29th April, 2020: Initial version