Monday, January 14, 2008

Exception Safety in C#

C# vs. C++

C# tries to perform resource management through finally clauses and the garbage collector. This is radically different from C++'s approach. C++ executes destructors (which do all of the resource cleanup in modern C++) in a deterministic order.

While exception safety is certainly possible in C#, it is potentially error-prone. It frequently relies on the user of a class putting the correct code in a finally block. (This is code that the compiler frequently figures out in C++/CLI, so it's not a problem with the CLR.) C++, on the other hand, relies on the author of a class to put the correct code in the constructor and destructor.

C# requires that resource management be done every time a programmer uses a class. C++ requires that it be done once. This lies at the heart of my complaint with C#, but it gets worse, as we'll see.

To be fair, C# does handle one resource automatically. Memory management doesn't require any work since it's all cleaned up by the garbage collector. All other resources require some extra effort on the part of the programmer. C++ requires that programmers put all memory resources inside an appropriate smart pointer.

The conventional C# approach

Code that allocates a non-memory resource in C# is supposed to look like this:

// Declare all variables that may require cleanup at the top of your function.
// Also include enough state information that you'll know how to undo
// partial changes that were made.
try {
  // Use the variables.
} catch( Exception ) {
  // Roll back any changes that were made.
} finally {
  // Call Dispose on all of the objects created that implement Dispose.

Needless to say, this sucks. It requires that you know which objects require disposal. It forces you to declare all of your variables at the top of the function and move them away from where they are used. It's also a lot of extra text that makes it harder to figure out what the function is actually doing.

However, assuming that the programmer didn't screw up, it will work.

What happens when someone screws up?

Unfortunately, the burden of cleaning up resources is placed on the user of the resource rather than the implementor of the class that handles the resource. This means that you can't be sure that Dispose will be called every time that it has to be called. Furthermore, it means that resource cleanup has to be performed correctly more than once per class.

The CLR has anticipated this problem to some extent. It allows a class to declare a finalize function which gets called by the GC right before it gets destroyed. You won't know when this function will be called, but at least your resource will get cleaned up eventually if you use this.

Is Finalize enough?

Unfortunately, Finalize has some serious problems. Because the garbage collector destroys the objects in a non-deterministic order, some of the member variables of the object being finalized might have been destroyed already.

The CLR provides some information about the order in which objects are destroyed, however. The following assumptions can be used:
1. Objects that derive from CriticalFinalizerObject and have a finalization method can use references to objects without a finalization method.

2. Objects that do not derive from CriticalFinalizerObject can use references to objects that do derive from CriticalFinalizerObject and also objects that do not have finalization methods.

Microsoft strongly recommends that you don't derive anything from CriticalFinalizerObject. However, this is the only mechanism they give you to order the finalization methods. If you have one object that must finalize before another, then you either have to use this, or you have to do what Microsoft did for System.IO.StreamWriter. This class does not flush its buffer during finalization because it may contain a FileStream object that must be closed after the StreamWriter flushes its buffer. In other words, the StreamWriter simply doesn't work unless it is Disposed correctly.

Of course, if you have three objects who's finalization order must be determined ahead of time, then even deriving from CriticalFinalizerObject won't work. In other words, even if you didn't care exactly when a resource was cleaned up, finalize isn't enough to make all classes safe to use. Sometimes you must rely on the user of your class to make it safe in C#. The fact that it works a lot of the time is only going to make the users careless.

Personally, I find this scary.

This originally appeared on the PC-Doctor blog.