There are a lot of blog posts and articles about the proper IDisposable implementation, so why writing another one? I guess I am writing it because the topic is quite subjective as there are many different scenarios for IDisposable usage.
I am not going to explore all the details about IDisposable implementation. I would recommend the following readings though I would advise you to be a selective reader so don’t take everything for granted truth:
- DG Update: Dispose, Finalization, and Resource Management from Joe Duffy
- Implementing IDisposable and the Dispose Pattern Properly from Scott Dorman
These are valuable articles because they contain a lot of different opinions. I would also recommend the following two MSDN links which you may find contradicting:
- Implementing a Dispose Method (.NET 3.0 version)
- Implementing a Dispose Method (.NET 4.5 version)
While the most people talk about Dispose and Finalize patterns as different things I would recommend to be careful and to think about them as a single pattern. It is always hard, even impossible, to predict how your code will be used from other software developers. So, having a safe strategy and implementing Dispose and Finalize patterns together might be the right choice.
Let’s see the following class definition:
public class ProcessInfo : IDisposable { [DllImport("kernel32")] static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId); [DllImport("kernel32")] static extern int GetCurrentProcessId(); [DllImport("kernel32")] static extern bool CloseHandle(IntPtr hHandle); private readonly IntPtr hProcess; public ProcessInfo() { const int PROCESS_VM_READ = 0x10; this.hProcess = OpenProcess(PROCESS_VM_READ, false, GetCurrentProcessId()); } public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (this.hProcess != IntPtr.Zero) { CloseHandle(hProcess); } } }
It is not the best example but let’s pretend this class has some useful methods and properties for the current process. Because the class implements IDisposable interface it is reasonable to expect something like the following code:
using (var procInfo = new ProcessInfo()) { // call some methods and properties }
The code fragment above seems all good and fine. The tricky part is that the C# compiler will actually emit the following equivalent code:
ProcessInfo procInfo = null; try { procInfo = new ProcessInfo(); // call some methods and properties } finally { if (procInfo != null) { procInfo.Dispose(); } }
Let’s focus on the constructor. The main purpose of the constructor is to construct an object. This means that the constructed object should be in a usable state. This concept is deeply embraced in .NET base class library. Let’s see the following code fragment:
var fs = new FileStream("", FileMode.Open);
As you can guess, it will throw ArgumentException. Let’s see another canonical example:
public class Person { public Person(string name, uint age) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("invalid name", "name"); } if (age < MIN_AGE) { throw new ArgumentException("invalid age", "age"); } // store "name" and "age" } // Some useful methods and properties }
Most of us have written such code. Often it is perfectly legal to throw an exception when we cannot construct a usable object. Back to our example:
using (var procInfo = new ProcessInfo()) { // call some methods and properties }
If an exception is thrown in the constructor then procInfo variable won’t be assigned and therefore Dispose() method won’t be called. Let’s modify ProcInfo constructor a bit:
public ProcessInfo() { const int PROCESS_VM_READ = 0x10; this.hProcess = OpenProcess(PROCESS_VM_READ, false, GetCurrentProcessId()); // something wrong throw new Exception(); }
Oops, we have Win32 handle leak. One option is to use try-catch clause inside the constructor and call CloseHandle(…) method. Sometimes this is the best option.
Another option is to implement the Finalize pattern. It is easy and I find it more clear:
~ProcessInfo() { this.Dispose(false); }
Now, there are clear roles for the constructor and Dispose/Finalize methods and the Win32 handle leak is fixed.