Introduction
In non-garbage collected languages like C++, programmers must take care of memory and resource management, you know exactly when your objects are destroyed, implicitly (scope rules) or explicitly (object deletion). The counterpart is that working with pointers commonly leads to memory leaks and resources never freed. Opposed to this, the .NET environment implements a garbage collector that automates memory management and handles unused object destruction, making the programmer's life simpler. Objects are not destroyed when they are out of scope, and you can�t destroy them explicitly. It�s the GC�s job to destroy them and free the memory used, this is called �collecting� and it is done under certain circumstances that won�t be explained here. This means that the GC collects when needed, and you have no control of the destruction of your objects.
Therefore, there are no special considerations to take when writing a class that doesn't make use of resources. But when your class holds a critical resource, you must release, you just can't rely on the GC because you can�t determine when it will run. The GC, upon collection, calls the finalizer of the object (in C#, the finalizer is the object destructor), thus, this is not a good place to release the resources owned by an object. In the .NET class library, the garbage collector is implemented as System.GC
in the assembly mscorlib
.
Implementing IDisposable
There is an interface in .NET you must implement to "mark" your class in order to provide a mechanism to release resources. This interface is IDisposable
(its full name is System.IDisposable
and can be found in assembly mscorlib
). And it has only one method you must implement: Dispose()
. This method does the cleaning work in your class. The destructor should call Dispose()
to ensure a clean exit in case you forgot to explicitly call it, but not if it has already been called. A proposed implementation is:
public class A : IDisposable
{
protected bool disposed = false;
public A()
{
}
~A()
{
Dispose(false);
}
public void doSomething()
{
if (disposed)
throw new ObjectDisposedException("object's name");
else
{
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
}
disposed = true;
}
}
}
A virtual method Dispose(bool)
manages resource deallocation for both managed and unmanaged resources. disposing
is a parameter to tell who called the method. Dispose()
will call Dispose(true)
and the destructor will call Dispose(false)
. When instantiating class A
, all resource acquisition is made in the constructor, and when we are done with the object, we need to call Dispose(true)
from the public method Dispose()
to release all (both managed and unmanaged) resources. The destructor, called by the GC, also calls Dispose(false)
to clean unmanaged resources to ensure there are no leaks in our application. Calling GC.SuppressFinalize()
tells the GC not to call the object's destructor, for it just does what we have already done. Dispose(bool disposing)
is declared as virtual
so our derived classes can call it. disposed
is a flag to avoid calling Dispose()
more than once and to avoid using a released resource, an ObjectDisposedException
is thrown if this happens. Note that this implementation is not thread-safe, race conditions can happen.
The need to implement Dispose(bool)
arises because an object may hold both managed objects that need to be disposed and unmanaged resources. So, if you call Dispose(true)
, you take care of all resources owned and that's all, but if you don't, the GC will take care and the owner object should no longer call Dispose(true)
on each of the owned managed objects because they may be already finalized. But you must always clean unmanaged resources by calling Dispose(false)
.
Derived classes can easily extend this implementation:
public class B : A
{
public B()
{
}
~B()
{
}
protected override void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
}
base.Dispose(disposing);
}
}
}
Creating an instance of B
calls B
's constructor, which in turn calls A
's default constructor before exiting. B
's destructor does nothing, it only follows the destruction chain calling A
's destructor (in fact, we can obviate it, and when the GC claims the object, it will call A
's destructor because it's inherited by B
). Anyway, we reach A
's destructor where Dispose(false)
is called. But Dispose(bool)
is overridden, it was declared as virtual
in A
so the right Dispose(bool)
in the right class is called in A
's destructor. If it weren't declared as virtual
, A.Dispose(bool)
would be called thereby not calling B.Dispose(bool)
and never releasing B
's unmanaged resources.
Taking a closer look to our example, the output of Ildasm.exe says:
(~A()
is translated as A.Finalize()
when compiled to IL).
.method family hidebysig virtual instance void Finalize() cil managed
{
.maxstack 2
.try
{
IL_0000: ldarg.0
IL_0001: ldc.i4.0
IL_0002: callvirt instance void Test.A::Dispose(bool)
IL_0007: leave.s IL_0010
}
finally
{
IL_0009: ldarg.0
IL_000a: call instance void [mscorlib]System.Object::Finalize()
IL_000f: endfinally
}
IL_0010: ret
}
The compiler automatically generates code to control exceptions, it puts our code inside a try
/finally
block ensuring chain destruction by calling the base class destructor inside the finally
block. Also, if the virtual call to Dispose(bool)
throws an exception and it is not handled, the program continues with the execution flow. If exceptions are not properly handled inside every Dispose()
method, resource leaks can occur.
The using statement
C# has the using
statement that allows you to acquire one or multiple resources, use them in a block, and automatically call Dispose()
in each of them.
public class AppClass
{
public static void Main()
{
using (B a = new B())
{
Console.WriteLine(a.ToString());
};
}
}
This block of code translates to IL in the following manner:
.method public hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 1
.locals init ([0] class Test.B a)
IL_0000: newobj instance void Test.B::.ctor()
IL_0005: stloc.0
.try
{
IL_0006: ldloc.0
IL_0007: callvirt instance string [mscorlib]System.Object::ToString()
IL_000c: call void [mscorlib]System.Console::WriteLine(string)
IL_0011: leave.s IL_001d
}
finally
{
IL_0013: ldloc.0
IL_0014: brfalse.s IL_001c
IL_0016: ldloc.0
IL_0017: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_001c: endfinally
}
IL_001d: ret
}
That is the same as coding:
B a = new B();
try
{
Console.WriteLine(a.ToString());
}
finally
{
if (a != null)
a.Dispose();
}
using
also allows declaring more than one identifier of the same type:
using (B a1 = new B(), a2 = new B())
{
}
The using
statement executes the statement block disposing the objects when the end of the statement is reached or when an exception is thrown.
Conclusions
The non-deterministic object destruction provided by the GC forces us to take special care when working with objects wrapping resources:
- Implementing
IDisposable
. IDisposable
provides the contract a class must implement for correct resource use.
Dispose()
calls Dispose(true)
to release all resources and tells the GC not to call the destructor.
- Declaring
Dispose(bool)
as virtual
and calling the inherited method from the derived classes.
- Ensuring
Dispose()
invocation through the using
statement.
- Calling
Dispose(false)
in the destructor. Just in case...
- Calling
Dispose()
more than once shouldn�t do anything.
- Making use of a disposed object should throw
ObjectDisposedException
.