Destructors and why you should avoid them

One of my previous blog posts discussed some of the basic methods defined on every object – more specifically those related to equality. This blog post will focus on another one of the Object methods: the destructor. We will learn what it does, how to use it properly, and which alternatives are available. A destructor is also known as a finalizer in .NET jargon. Some people have strong opinions about the purported differences between the two, but in this blog post you can treat them as synonyms.

This post builds on the knowledge we gathered in the previous post concerning managed and unmanaged memory, so make sure to read that one first.

Definition

In short, destructors are responsible for cleaning up resources that are not managed by the Common Language Runtime (CLR) at the end of an object’s lifecycle. Further down we will talk about this in more detail.

All destructors must respect two very important invariants. First, there must be at most one destructor per class. Second, the destructor of a class – if present – can only be called by the CLR. In other words, developers cannot invoke destructors manually.

The C# destructor definitions reflect these two invariants, as can be seen in object.cs:

~Object()
{
}

Note how this destructor accepts no parameters. In addition, the C# language specification states that destructors cannot be overloaded. These two facts combined enforce the first invariant. Note also how the accessibility modifier seems to be missing. In fact, the specification states that destructors cannot have explicit modifiers. Their accessibility domain must be empty to enforce the second invariant.

In an object hierarchy, each subclass can define its own destructor, but this destructor does not override the base destructor in the traditional sense. Instead, the runtime automatically calls the destructors from most derived to least derived class (at some point, maybe).

A destructor can only be defined on a reference type, not a value type. The problem is the pass-by-value semantics of value types. This violates the RAII idiom: one resource, one encapsulating object owning that resource. If multiple objects reference the same resource, who is the owner that will eventually free the resource? There are no easy solutions here, so the language designers simply chose not to make this possible. If you must handle unmanaged resources, wrap them in a class.

Syntactic sugar

The syntax for a destructor in C# looks surprisingly similar to the C++ destructor syntax. This was a conscious choice by the language designers, but that was not without risk. Destructors in C# behave very different from destructors in C++, so perhaps a different syntax would not have been such a bad idea. On the other hand, destructors also require expert knowledge to implement them properly, so exposing them as regular methods would make them too approachable.

Surprisingly, the destructor cannot be found in the MSDN documentation for Object. Instead, the method list contains protected virtual void Finalize(). As it turns out, the destructor syntax is but C#-specific syntactic sugar for the Finalize method. Visual Basic does not support this syntactic sugar, so destructors are implemented by overriding this Finalize method instead. The base Finalizer must be called explicitly on the last line of this method to ensure finalization happens in the correct order.

To see that the Finalize method is still present in C#, though hidden, you can try to call it explicitly using this.Finalize(). Instead of the expected “does not contain a definition for” error, you will find a custom error message pertaining to destructors. Additionally, declaring your own void Finalize() method also has some curious effects. Under normal circumstances it only generates a warning telling you that this method can potentially interfere with destructor invocation. When effectively adding a destructor, that warning turns into an error telling us that “a member with the same signature is already declared”. This confirms that a destructor is syntactic sugar for Finalize.

Destructors in a managed language

The C# garbage collector only manages resources on the managed heap. However, C# code regularly uses unmanaged resources. Examples include handles to files, windows, registry keys, processes, network- or database connections. When these resources are no longer needed, cleaning them up must be handled explicitly to prevent memory leaks. This is why C# still has destructors, whose sole responsibility is to deal with unmanaged resources. The Base Class Library (BCL) offers managed wrappers (e.g. safe handles) for many of these unmanaged resources, so developers will rarely have to deal with them directly.

C++ and C# both use the concept of destructors, but there are significant differences between both implementations. In C++, destructors run on the user thread, either when the associated variable goes out of scope in case of stack allocation, or when the object is explicitly deleted if it lives on the free store. Furthermore, the destructor can only run after a constructor has been executed successfully. This makes C++ destructors relatively easy to comprehend.

C# handles these matters very differently. When an object with a destructor dies because it is not referenced anymore, nothing happens until the GC detects this. Even when that happens, that destructor is not executed immediately and the dead object’s memory is not reclaimed yet either. Instead, the object is added to the ReadyToFinalize queue. Next, when the finalizer thread runs, the objects in that queue will be finalized one by one by executing their destructor. This continues until all objects are finalized or the runtime gives priority to another thread. Once the destructor has completed, the object will be picked up again by the following GC run and the memory can finally be reclaimed. This whole process can bring along a significant amount of overhead, especially if the object to be finalized is preventing lots of other objects it references to be collected during the current run.

To avoid this overhead, do not add a destructor to classes that do not really need one. Astute readers may point out that because the Object class contains a destructor and all classes inherit from Object, every object will eventually end up on the ReadyToFinalize queue. This is obviously not the case, so the CLR must have implemented a workaround. Perhaps it treats the destructor in Object as an exception, or – as a more general solution – it ignores all destructors with an empty body.

Clearly, C# destructors are quite a bit more complex and less predictable than their C++ counterparts. They are very hard to get right, especially because many common programming intuitions do not apply to them.

  • Because destructors run on the finalizer thread, there is the risk of deadlocks and also exception handling difficulties.
  • Destructors are not guaranteed to run. The finalizer thread is at the mercy of a thread scheduling algorithm, and there can potentially always be a higher priority thread waiting. Furthermore, the GC.SuppressFinalize method can be used to disable finalization of a finalizable object.
  • Destructors can be executed multiple times, for example by calling the GC.ReRegisterForFinalize method.
  • The last two bullet points combined show that a destructor can be executed an arbitrary number of times.
  • Objects whose constructor was interrupted somehow can still be finalized, so class invariants are not guaranteed to be enforced during destruction. Even worse, in exceptional cases the destructor might be called while the constructor is still running.
  • Objects in the ReadyToFinalize queue were once dead, but could potentially be resurrected before, during or after the execution of the finalizer. This means user code could interact with the object (from another thread) from resurrection onward.
  • Certain actions in a destructor might resurrect dead objects. In very rare cases this might be the right thing to do, but it is strongly recommended to avoid this.

In conclusion, stay away from destructors if at all possible. There is no need for them if you are not dealing with unmanaged memory directly. If you are, check whether the BCL contains some managed wrapper that you can use instead. Even if such wrapper does not exist, there is a good alternative technique. Read the next blog post to learn more.

Further reading

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s