I‘ve heard several people asking why GCHandle doesn‘t implement IDisposable, considering it wraps an unmanaged resource (a handle) and needs to be explicitly freed (using GCHandle.Free()). Before I explain the reason, I want to give a little background on GCHandles and their dangers.
What‘s a GCHandle?
A GCHandle is a struct that contains a handle to an object. It‘s mainly used for holding onto a managed object that gets passed to the unmanaged world to prevent the GC from collecting the object. You can also create a Pinned GCHandle to a managed object and retrieve the object‘s address in memory.
How are GCHandles dangerous?
When you create a new GCHandle, a new entry in the AppDomain‘s handle table is created. This entry is kept until the handle is freed (via GCHandle.Free()) or the AppDomain is unloaded.
Things get tricky if you were to make a copy of a GCHandle:
Object obj = new Object();
GCHandle gch = GCHandle.Alloc(obj, GCHandleType.Normal);
GCHandle gch2 = gch;
Since
GCHandle is value type, gch2 has its own copy of the handle. You now
have two handles that point to the same entry in the handle table.
Unfortunately, since gch2 is a copy of –not a reference to– gch,
anything that happens to gch doesn‘t happen to gch2. For example,
calling gch.Free() will delete the entry from the handle table, but not
update gch2, so gch2.IsAllocated will return true, but gch2.Target will
be null. The same problem arises with casting to and from IntPtrs, and
when GCHandles get boxed. Unlike double-freeing a single GCHandle,
freeing the copy will NOT throw an InvalidOperationException. You have
to be very careful not to double-Free your handles since this can
corrupt the handle table.
Why don‘t GCHandles implement IDisposable?
One
of the main purposes of IDisposable to avoid the use of finalizers.
This is because finalizers are not run deterministically, and result in
promoting a finalizable object a generation, effectively keeping in
memory longer. Since GCHandle is a value type, it has no finalizer, and
is not collected by the GC, so these problems are eliminated. Another
other main use of IDisposable is to clean up unmanaged resources as soon
as you are done with them. With a GCHandle, the resource is the handle
which is cleaned up by calling GCHandle.Free(). If Free isn‘t called,
the handle gets cleaned up when the appdomain is unloaded.
One
of the side effects of having a struct implement IDisposable, is that
users may be tempted to cast their GCHandles as IDisposable, which boxes
the GCHandle into an IDisposable object on the heap, and the two
GCHandles get out-of-sync. The same problem arises with the putting a
disposable struct into a using block:
struct Test : IDisposable
{
public bool disposed; // initialized to false
public void Dispose()
{
disposed = true;
}
}
public void Foo()
{
Test t = new Test();
using (t)
{
// do stuff
}
if (!t.disposed)
{
t.Dispose();
}
}
t.disposed
will return false, since it was a copy of t whose Dispose method was
called. If t were a GCHandle, then the handle would be removed from the
appdomain‘s handle table, and calling Free after the using would result
in a double Free, even though IsAllocated would return true!
Remember,
GCHandles are advanced structures, and one should be very careful to
ensure they are cleaned up properly. Unfortunately, IDisposable makes it
easy to get this wrong, so the BCL designers erred on the side of
caution, and gave GCHandle a Free() method to use.