C# calls them finalizers and that’s strangely appropriate because all you really know about them is that they might eventually be called, finally. This is because C# decided to manage object lifetimes for you so you normally don’t need to worry about leaking memory anymore. This is called garbage collection. The problem with this is that you now have other things to worry about.
C++ is notorious for memory leaks so the designers of C# decided to fix this by cleaning up any used memory for you. But memory is just part of what destructors help take care of. In order to control exactly when files get closed or other resources are returned, C# provides a method called Dispose.
The Dispose method is part of the Disposable interface. There are some gotchas to worry about with calling Dispose. This episode explains how you should call Dispose as part of a “using” statement. This will guarantee that Dispose will be called even if an exception is thrown. Listen to the full episode or read the full transcript below.
C# is a great language that offers some big productivity gains. But like any engineering project, there’s always tradeoffs. C++ is notorious for memory leaks, so the designers of C# wanted to fix this. The solution was to let the runtime manage all memory allocations. In fact, you don’t even have access to pointers in C# unless you drop into unsafe mode. In C#, destructors are called whenever the runtime determines that the object instance is no longer being used. But the runtime uses its own schedule for this.
Garbage collection brings up images of household trash bundled in trash bags sitting on the roadside curb waiting to be picked up. That’s not really how managed code works though. A better analogy would be for you to just leave your wrappers and trash laying around when you’re done. Finished eating that fast food burger? You don’t even need to crumple up the wrapper. Just let go of it and let it fall wherever. The managed code runtime will periodically walk through your house, reach into your car, and generally go places no curbside pickup service would ever be allowed.
That’s great for memory, at least normally, but what about that other aspect of destructors. What happens when you open a file or need to commit your changes to a database. You still want, and really need, those actions to take place at specific times that you can count on. Only with C#, you can’t count on when your destructors will be run.
Because the runtime has its own schedule, it can sometimes investigate an object and find that you’re legitimately still using it. So it’s not a candidate for destruction. In order to improve performance, the creators of C# decided that the runtime just couldn’t keep investigating every single object instance every time. So when it finds an object still being used, it moves that object to a new survivor’s list that will be checked at some future time to see if the object is still being used or not. If it’s still being used, then it gets moved to yet another list that gets investigated much less frequently. Each of these lists is called a generation. There’s generation zero, one, and two. Generation zero objects get investigated quickly and because of this C# is optimized for short object lifetimes. This means that C# works really well when you create an object, use it quickly, and then forget about it right after that.
The problem though is that files, databases, maybe some elaborate calculations tend to create objects that pass through generation zero and will be looked at later. So the very things you need precise control over making sure that resources are released, are the things that you tend to have the most trouble with.
It’s not all lost though. There is a solution that most C# developers learn about early on. What’s not always appreciated is the engineering tradeoff that went into this design.
The solution is to declare support for something called IDisposable. This is an interface and we haven’t talked about interfaces yet. I’ll just say for now that an interface is like an advertisement that your class will have support for the capabilities declared in the interface. What we’re looking for here is a method called Dispose.
When your class implements IDisposable, you’re actually declaring that your class will have a method called Dispose. Think of dispose sort of like the destructor that you don’t normally think of in C#. C# does still have destructors but gives them the new name finalizers. A C# finalizer looks a lot like a C++ destructor at least in the method signature. But where you don’t normally ever need to call a C++ destructor, you do need to take a more active role in calling Dispose.
This is where I always feel like this approach breaks the object-oriented encapsulation. With C++, the class can clean up it’s own resources in a reliable manner. But with C#, you now have to know to call Dispose yourself. The class is no longer as self contained. It relies on some other code to help it clean up its used resources.
It actually gets a little more complicated than this though. Because both C++ and C# have support for exceptions as a form of error handling. We haven’t talked in depth yet about exceptions but know this for now: exceptions have the ability to bypass methods you might have been planning to call. This means that you might have your code written with what you thought was the responsible approach and will call Dispose at just the right time. Only before you get the chance to call Dispose, an exception is thrown that takes your carefully planned methods and just jumps right over them. You end up missing your call to Dispose just because some error situation was detected. This error might have been completely unrelated to the file you need to close.
That’s why C# added support for a special using statement. You declare that you are “using” a variable instance and the runtime will make sure to call Dispose even if an exception is thrown.
In the end, you sort of have support for functionality similar to destructors in C#. You just have to make sure to write your code to take advantage of IDisposable.