Clean Up Your .NET With the Dispose Pattern
Nick Proud
Posted on June 15, 2020
In the managed-code world of .NET, we can be sometimes spoiled by the seamless intervention of Garbage Collection which quite happily waits until our objects are no longer referenced and then scoops them up and destroys them. That 'Person' object you're no longer referencing? Gone. That 'Tesla' object you lovingly inherited from 'Car' and then added loads of 'door' properties to? Obliterated. See ya.
What about when our old-buddy-old-pal the collector can't pick up our trash? What do we do when we are using a resource which is unmanaged and therefore cannot be picked up for us?
In these scary, less comfortable unmanaged situations, it's time for us to roll up our sleeves and muck in with the dispose pattern.
Unmanaged Resources
Most of the time when we are working with .NET, we are in a managed environment, which means that once an object has no references, it becomes 'orphaned.' From this point on, they become a candidate for garbage collection, meaning that whenever the amount of memory in use goes over a particular threshold, .NET identifies orphaned objects and disposes of them for you.
But this is not necessarily the case for some resources, in particular resources that represent interactions with the operating system, such as operations on files or connections to a database. For these kinds of resources, not cleaning up after ourselves when we are done can become a problem.
IDisposable and the Dispose Pattern
.NET gives us many handy interfaces which we can implement to enforce a contract of expected behaviour. For managing the disposal of resources, we can implement the IDisposable interface. Implementing IDisposable provides us with a means of releasing our hold on any unmanaged resources we no longer need, so as to conserve memory and ensure that we do not disrupt the flow of our program.
Implementing IDisposable forces us to add a Dispose() method. This method can sit in a class in which we expect to use unmanaged resources. We can provide whatever logic is required in order to release the resource in this method. In the example below, we close out database connection in the Dispose method.
public static SqlConnection connection = new SqlConnection(connectionString); public void SaveToDB(Metric metric) { connection.Open(); //do stuff } void Dispose() { connection.Close(); }
Please don't do it like that.
Disposing of our SQL connection in the manner shown above is a bad way to dispose of unmanaged resources. We would most likely have to call Dispose manually. This is not always avoidable, but in this scenario, there is a better way.
In C#, the using statement allows us to instantiate an object representing an unmanaged resource, and then use that object in a block of code which will automatically dispose of the object when it falls out of scope. So let's change our example above to dispose of the SqlConnection object as soon as we are finished with it.
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
//do stuff
}
//out of scope, connection is disposed.
As long as the enclosing type representing the unmanaged resource (in this case SqlConnection) implements IDisposable, we can use the using statement to cleanly dispose of objects without having to call the method ourselves.
What if we still need to use Dispose()?
You might find situations where you need to use an unmanaged resource within a custom class in which you need to implement IDisposable and work with Dispose manually. If this happens, there are a few extra scenarios to consider.
The Dispose method can be called directly or indirectly by our code, meaning that we are controlling when the resource is being disposed of and the manner in which it is disposed of. However, the dispose method could also be called automatically by the runtime, as part of a 'finalizer.'
Finalizers
When the garbage collector releases an unmanaged resource it will 'finalize' the process of disposing of the object automatically for us. However, when it comes to unmanaged resources, it does not. This is where issues can begin to occur if for some reason Dispose was not called by a consumer of a type using unmanaged resources. The finalization process would have no knowledge of that open SQL connection and it would just stay there. However, if we could tell the finalization process what steps it needs to take to kill that DB connection, that would be much better. We can override the normally automatic call to the Garbage Collector's Finalize method with the use of a destructor. A destructor is simply a method named after the object in which resides preceded by a tilde (~) which will run on finalization.
~DBConnectorClass()
{
connection.Close()
//finalized.
}
This is great for cleaning up our unmanaged resource when the garbage collector starts to do its rounds, but what if we have a situation where we have a finalizer, but a consumer of our type could also call Dispose?
Dispose() Overload
We can call Dispose and pass a boolean value to indicate whether the call is coming from a consuming type for disposal (true) or whether the finalizer is running through its normal process (false)
If the boolean evaluates to true, a block will run for the purpose of releasing managed objects. Following this, regardless of true or false the unmanaged objects will be released. Microsoft outline a really good example below:
using System;
class DerivedClass : BaseClass
{
// Flag: Has Dispose already been called?
bool disposed = false;
// Protected implementation of Dispose pattern.
protected override void Dispose(bool disposing)
{
if (disposed)
return;
if (disposing) {
// Free any other managed objects here.
//
}
// Free any unmanaged objects here.
//
disposed = true;
// Call the base class implementation.
base.Dispose(disposing);
}
~DerivedClass()
{
Dispose(false);
}
}
Notice that there is a destructor at the bottom of the class above. This means that if one of our consuming types called Dispose (passing true), a finalizer would no longer need to be run as part of garbage collection. But the runtime doesn't know that. So, we can tell it so. Below, you can see that our non-overloaded Dispose method is still in use, and would take care of our initial call with Dispose(true) but then also uses a method in the GC class to tell the runtime that finalizers no longer need to run for the object we are passing in. In this case, this, meaning the current instance of the object in which the method resides.
public void Dispose()
{
// Dispose of unmanaged resources.
Dispose(true);
// Suppress finalization.
GC.SuppressFinalize(this);
}
Bossing the garbage collector around
GC.SuppressFinalize is one of many ways we can explicitly instruct the garbage collector to deviate from its default behaviour. There are many more methods we can use.
GC.KeepAlive() tells the garbage collector that a specific object is not eligible for collection.
On the flip-side, GC Collect() forces garbage collection. This is particularly useful in some scenarios where you have a long-running set of processes. It is difficult to know when the garbage collector is going to kick in, and sometimes it can be a resource-intensive process.
If you don't want to risk having a collection during an important operation. GC.Collect() can make sure we force a collection so that one is not needed again for some time.
I find it tricky to fully understand myself
There's no doubt that automatic garbage collection is one of the major selling points of the .NET Framework. I'm not afraid to admit however that I personally find it a difficult subject to fully grasp. One of the reasons I decided to outline my interpretation of its functionality in a blog post was to hopefully better understand it and reaffirm what I do and don't know about something that is running constantly as I build software.
I'm sure I will have gotten something wrong somewhere and no doubt somebody will (hopefully kindly) correct me. It just goes to show, sometimes we can take the hand-holding .NET gives us for granted, making it harder to understand the immense array of tasks going on under the hood, and this has only been a little peek at a larger world of managed code. I think putting pen to paper on this one might make me brave enough to dig a little deeper.
Posted on June 15, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024