.NET tries to simplify the management of object lifecycles by relieving you of the need to explicitly de-allocate the memory occupied by their objects. However, simplifying the object lifecycle comes with potential penalties in terms of system scalability and throughput. If the object holds onto expensive resources such as files or database connections, those resources are released only when Finalize()
(or the C# destructor) is called. This is done at an undetermined point in the future, usually when certain memory-exhaustion thresholds are met. In theory, releasing the expensive resources the object holds may never happen, thus severely hampering system scalability and throughput.
There are a few solutions to the problems arising from nondeterministic finalization. These solutions are called deterministic finalization, because they take place at a known, determined point in time. In all deterministic finalization techniques, the object has to be explicitly told by the client when it’s no longer required. This section describes and contrasts these techniques.
In order for deterministic finalization to work, you must first implement methods on your object that allow the client to explicitly order cleanup of expensive resources the object holds. Use this pattern when the resources the object holds onto can be reallocated. If this is the case, the object should expose methods such as Open()
and Close()
.
An object encapsulating a file is a good example. The client calls Close()
on the object, allowing the object to release the file. If the client wants to access the file again, it calls Open()
, without re-creating the object. The classic example of classes that implement this pattern are the database connection classes.
The main problem with using Close()
is that it makes sharing the object between clients a lot more complex than COM’s reference counting. The clients have to coordinate which one is responsible for calling Close()
and when it should be called—that is, when it is safe to call Close()
without affecting other clients that may still want to use the object. As a result, the clients are coupled to one another. There are additional problems, as well. For example, some clients may interact with the object only using one of the interfaces it supports. In that case, where should you implement Open()
and Close()
? On every interface the object supports? On the class directly, as public methods? Whatever you decide is bound to couple the clients to your specific object-finalization mechanism. If the mechanism changes, the change triggers a cascade of changes on all the clients.
The more common case is when disposing of the resources the object holds amounts to destroying the object and rendering it unusable. In that case, the convention is for the object to implement a method called Dispose()
, defined as:
void Dispose();
When a client calls Dispose()
, the object should dispose of all its expensive resources, and the disposing client (as well as all other clients) shouldn’t try to access the object again. In essence, you put in Dispose()
the same cleanup code you put in Finalize()
(or the C# destructor), except you don’t wait until garbage-collection time for the cleanup.
If the object’s base class has a Dispose()
method, the object should call its base-class implementation of Dispose()
to dispose of resources the base class holds.
The problems with Dispose()
are similar to those with Close()
. Sharing the object between clients couples the clients to one another and to the object-finalization mechanism, and again, it’s unclear where should you implement Dispose()
.
A better design approach to deciding where and how you should implement Dispose()
is to factor the method to a separate interface altogether. This special interface (found in the System
namespace), called IDisposable
, is defined as:
public interface IDisposable { void Dispose(); }
In the object’s implementation of IDisposable.Dispose()
, the object disposes of all the expensive resources it holds:
public interface IMyInterface { void SomeMethod(); } public class MyClass : IMyInterface,IDisposable { public void SomeMethod() {...} public void Dispose() { //Do object cleanup and call base.Dispose() if it has one } //More methods and resources }
Having the Dispose()
method on a separate interface allows the client to use the object’s domain-specific methods and then query for the presence of IDisposable
and always call it, independent of the object’s actual type and actual finalization mechanism:
IMyInterface obj = new MyClass(); obj.SomeMethod(); //Client wants to dispose of whatever needs disposing: IDisposable disposable = obj as IDisposable; if(disposable != null) { disposable.Dispose(); }
Note the defensive way in which the client calls Dispose()
, using the as
operator. The client doesn’t know for certain whether the object supports IDisposable
. The client finds out in a safe manner, because if the object doesn’t support IDisposable
, as
returns null
. However, if the object does support IDisposable
, the client would like to expedite disposing of the expensive resources the object holds. The clear advantage of IDisposable
is that it further decouples the client from the object-finalization mechanism and provides a standard way to implement Dispose()
. However, the disadvantage is that sharing objects between clients is still complicated, because the clients have to coordinate among themselves who is responsible for calling IDisposable.Dispose()
and when to call it; thus, the clients remain coupled to each other. In addition, a class hierarchy should implement IDisposable
in a consistent manner—that is, implement it at each level of the class hierarchy and have every level call its base level’s Dispose()
.
Whether the object provides IDisposable
or just Dispose()
as a public method, the client should scope the code using the object and then dispose of the resources it holds in a try/finally
block. The client should put the method calls in the try
statement and put the call to Dispose()
in the finally
statement, as shown in Example 4-1. The reason is that calling methods on the object may cause an error that throws an exception. Without the try/finally
block, if this happens the client’s call to dispose of the resources will never be reached.
The problem with this programming model is that the code gets messy if multiple objects are involved, because each one can throw an exception, and you should still clean up after using them. To automate calling Dispose()
with proper error handling, C# supports the using
statement, which automatically generates a try/finally
block using the Dispose()
method. For example, for this class definition:
public class MyClass : IDisposable { public void SomeMethod() {...} public void Dispose() {...} /* Expensive resources here */ }
if the client code is:
MyClass obj = new MyClass();
using(obj)
{
obj.SomeMethod();
}
the C# compiler converts that code to code semantically equivalent to:
MyClass obj = new MyClass(); try { obj.SomeMethod(); } finally { if(obj != null) { IDisposable disposable = obj; disposable.Dispose(); } }
You can even stack multiple using
statements to handle multiple objects:
MyClass obj1 = new MyClass(); MyClass obj2 = new MyClass(); MyClass obj3 = new MyClass(); using(obj1) using(obj2) using(obj3) { obj1.SomeMethod(); obj2.SomeMethod(); obj3.SomeMethod(); }
The using
statement has one liability: the compiler-generated code either uses a type-safe implicit cast from the object to IDisposable
, or it requires that the type passed in provide a Dispose()
method. That precludes using the using
statement with interfaces in the general case, even if the implementing type supports IDisposable
:
public interface IMyInterface
{
void SomeMethod();
}
public class MyClass: IMyInterface,IDisposable
{
public void SomeMethod()
{}
publicd Dispose()
{}
}
IMyInterface obj = new MyClass();
using(obj)//This line does not compile now
{
obj.SomeMethod();
}
Three workarounds allow for combining interfaces with the using
statement. The first is to have all interfaces in the application derive from IDisposable
:
public interface IMyInterface : IDisposable
{
void SomeMethod();
}
public class MyClass: IMyInterface
{
public void SomeMethod()
{}
public void Dispose()
{}
}
IMyInterface obj = new MyClass();
using(obj)
{
obj.SomeMethod();
}
The disadvantage of this workaround is that the interface is now less factored.
Tip
Having all interfaces derive from IDisposable
is analogous to having every COM interface derive from IUnknown
so that the interfaces will have the reference-counting methods.
The second workaround is to coerce the type used in IDisposable
with an explicit cast to fool the compiler:
public interface IMyInterface
{
void SomeMethod();
}
public class MyClass: IMyInterface,IDisposable
{
public void SomeMethod()
{}
public void Dispose()
{}
}
IMyInterface obj = new MyClass();
using((IDisposable)obj)
{
obj.SomeMethod();
}
The problem with the explicit cast is that it is made at the expense of type safety, because if the underlying type does not support IDisposable
, you will encounter an invalid cast exception at runtime. You can use an explicit cast to IDisposable
only if you know for certain that the underlying type will support it. This, of course, negates separation of interface from implementation and introduces coupling between the client and the actual finalization mechanism used by the object that supports the interface.
The third and best workaround is to use the as
operator:
using(obj as IDisposable)
{
obj.SomeMethod();
}
As shown previously, the code the compiler generates for the using
statement checks that the variable passed in is not null
before proceeding to implicitly casting it to IDisposable
and calling Dispose()
. Because the as
operator returns null
if the underlying type does not support IDisposable
, incorporating the as
operator into the using
statement decouples the client from the underlying server type and actual finalization mechanism used and allows it to defensively dispose of the resources the object holds.
When you supply an object of a generic type parameter to the using
statement, the compiler has no way of knowing whether the actual type the client will specify supports IDisposable
. The compiler will therefore not allow you to specify a naked generic type for the using
statement:
public class MyClass<T>
{
public void SomeMethod(T t)
{
using(t)//Does not compile
{...}
}
}
When it comes to generic type parameters, you can actually constrain the type parameter to support IDisposable
, using a derivation constraint:
public class MyClass<T> where T : IDisposable { public void SomeMethod(T t) { using(t) {...} } }
The constraint ensures that the client specifies only type parameters that support IDisposable
. As a result, the compiler will let you use the type parameter directly in the using
statement. Even though you can certainly apply this constraint, I recommend against doing so. The problem with the constraint is that now you cannot use interfaces as generic type parameters, even if the underlying type supports IDisposable
:
public interface IMyInterface {} public class SomeClass : IMyInterface,IDisposable {...} public class MyClass<T> where T : IDisposable { public void SomeMethod(T t) { using(t) {...} } } SomeClass someClass = new SomeClass(); MyClass<IMyInterface> obj = new MyClass<IMyInterface>(); //Does not compile obj.SomeMethod(someClass);
Fortunately, you can use the as
operator with the using
statement on generic type parameters to enable its use when dealing with interfaces:
public class MyClass<T>
{
public void SomeMethod(T t)
{
using(t as IDisposable)
{...}
}
}
Dispose()
and Finalize()
(or the C# destructor) aren’t mutually exclusive, and in fact, you should actually provide both. The reason is simple: when you have expensive resources to dispose of, even if you provide Dispose()
there is no guarantee that the client will actually call it, and there is a risk of unhandled exceptions on the client’s side. Therefore, if Dispose()
isn’t called, your fallback plan is to use Finalize()
to do the resource cleanup. On the other hand, if Dispose()
is called, there is no point in postponing object destruction (that is, the de-allocation of the memory the object itself occupies) until Finalize()
is called. Recall that the garbage collector detects the presence of Finalize()
from the metadata. If a Finalize()
method is detected, the object is added to the finalization queue and destroyed later. To compensate for that, if Dispose()
is called, the object should suppress finalization by calling the static method SuppressFinalize()
of the GC
class, passing itself as a parameter:
public static void SuppressFinalize(object obj);
This prevents the object from being added to the finalization queue, as if the object’s definition didn’t contain a Finalize()
method.
There are other things to pay attention to if you implement both Dispose()
and Finalize()
. First, the object should channel the implementation of both Dispose()
and Finalize()
to the same helper method, to enforce the fact that it’s doing exactly the same thing regardless of which method is used for the cleanup. Second, it should handle multiple Dispose()
calls, potentially on multiple threads. The object should also detect in every method whether Dispose()
was already called, and if so refuse to execute the method and throw an exception instead. Finally, the object should handle class hierarchies properly and call its base class’s Dispose()
or Finalize()
.
Clearly, there are a lot of details involved in implementing a bulletproof Dispose()
and Finalize()
, especially when inheritance is involved. The good news is that it’s possible to provide a general-purpose template, as shown in Example 4-2.
Example 4-2. Template to implement Dispose() and Finalize() on a class hierarchy
public class BaseClass: IDisposable { private bool m_Disposed = false; protected bool Disposed { get { lock(this) { return m_Disposed; } } } //Do not make Dispose() virtual - you should prevent subclasses from overriding public void Dispose() { lock(this) { //Check to see if Dispose() has already been called if(m_Disposed == false) { Cleanup(); m_Disposed = true; //Take yourself off the finalization queue //to prevent finalization from executing a second time. GC.SuppressFinalize(this); } } } protected virtual void Cleanup() { /*Do cleanup here*/ } //Destructor will run only if Dispose() is not called. //Do not provide destructors in types derived from this class. ~BaseClass() { Cleanup(); } public void DoSomething() { if(Disposed)//verify in every method { throw new ObjectDisposedException("Object is already disposed"); } } } public class SubClass1 : BaseClass { protected override void Cleanup() { try { /*Do cleanup here*/ } finally { //Call base class base.Cleanup(); } } } public class SubClass2 : SubClass1 { protected override void Cleanup() { try { /*Do cleanup here*/ } finally { //Call base class base.Cleanup(); } } }
Each level in the class hierarchy implements its own resource cleanup code in the Cleanup()
method. Calls to either IDisposable.Dispose()
or the destructor (the Finalize()
method in Visual Basic 2005) are channeled to the Cleanup()
method. Only the topmost base class in the class hierarchy implements IDisposable
, making all subclasses polymorphic with IDisposable
. On the other hand, the topmost base class implements a non-virtual Dispose()
method, to prevent subclasses from overriding it. The topmost base class’s implementation of IDisposable.Dispose()
calls Cleanup()
. Dispose()
can be called by only one thread at a time, because it uses a synchronization lock (discussed in Chapter 8). This prevents a race condition in which two threads try to dispose of the object concurrently. The topmost base class maintains a Boolean flag called m_Disposed
, signaling whether or not Dispose()
has already been called. The first time Dispose()
is called, it sets m_Disposed
to true
, which prevents itself from calling Cleanup()
again. As a result, calling Dispose()
multiple times is harmless.
The topmost base class provides a thread-safe, read-only property called Disposed
that every method in the base class or subclasses should check before executing method bodies and throw an ObjectDisposedException
if Dispose()
is called.
Note that Cleanup()
is both virtual and protected. Making it virtual allows subclasses to override it. Making it protected prevents clients from using it. Every class in the hierarchy should implement its own version of Cleanup()
if it has cleanup to do. Also note that only the topmost base class should have a destructor. All the destructor does is delegate to the virtual, protected Cleanup()
. The destructor is never called if Dispose()
is called first, because Dispose()
suppresses finalization. The only difference between calling Cleanup()
via the destructor or via Dispose()
is the Boolean parameter m_Disposed
, which lets Dispose()
know whether to suppress finalization.
Here is how the mechanism shown in the template works:
The client creates and uses an object from the class hierarchy and then calls
Dispose()
on it by usingIDisposable
orDispose()
directly.Regardless of which level of the class hierarchy the object is from, the call is served by the topmost base class, which calls the virtual
Cleanup()
method.The call travels to the lowest possible subclass and calls its
Cleanup()
method. Because at each level theCleanup()
method calls its base class’sCleanup()
method, each level gets to perform its own cleanup.If the client never calls
Dispose()
, the destructor calls theCleanup()
method.
Note that the template correctly handles all permutations of variable type, actual instantiation type, and casting:
SubClass1 a = new SubClass2(); a.Dispose(); SubClass1 b = new SubClass2(); ((SubClass2)b).Dispose(); IDisposable c = new SubClass2(); c.Dispose(); SubClass2 d = new SubClass2(); ((SubClass1)d).Dispose(); SubClass2 e = new SubClass2(); e.Dispose();
Get Programming .NET Components, 2nd Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.