In this article, we will show how to correctly implement the IDisposable interface. We will focus on key aspects with a clear explanation.
The IDisposable interface in managed code gives us a tool to manage resource cleanup. By implementing IDisposable, we can also release any unmanaged resources held by our object.
As a C# programmer, we should be able to correctly implement the IDisposable interface using best practices, including the Dispose design pattern.
Let’s dive in.
The Purpose of the IDisposable Interface
The IDisposable
interface is primarily used to release resources in a timely fashion.
The .NET Garbage Collector (GC) manages memory excellently by unpredictably releasing unused variables. We could exhaust some scarce resources before GC kicks in; thus, we must handle that differently.
These resources involve handling files, connecting to databases and networks, and managing other unmanaged resources.
We must explicitly release these resources to avoid resource leaks and performance issues.
Basic Implementation of IDisposable
We can implement the IDisposable
interface straightforwardly in most scenarios. If our class implements IDisposable
, we must provide a Dispose()
method. In that method, we should include all the necessary logic for releasing unmanaged resources and references to other disposable managed objects.
We must release a resource deterministically without waiting for the GC to act.
In our case, our class MyClass
has a field of a type that we should dispose of. Because of that, our class should implement the IDisposable
interface:
public sealed class MyClass : IDisposable { private readonly IManagedResource _managedResource; public void Dispose() { Console.WriteLine($"Called {nameof(MyClass)}.{nameof(Dispose)}"); _managedResource.Dispose(); } }
As we can see, we only need to implement one method Dispose()
and call the Dispose()
method on our _managedResource
object inside of it. This version works well for sealed classes.
To improve our implementation slightly, we should dispose of our managed resources only once:
public sealed class MyClass : IDisposable { private bool _disposed = false; private readonly IManagedResource _managedResource; public void Dispose() { Console.WriteLine($"Called {nameof(MyClass)}.{nameof(Dispose)}"); if (_disposed) { return; } _managedResource.Dispose(); _disposed = true; } }
Now, we add a new field _disposed
and check inside the Dispose()
method if we have already disposed of this object (our flag will be true). If not, dispose of our resources and set this flag.
The Dispose Pattern
Imagine that our project is getting bigger, and we must support further inheritance. We do that by making the IDisposable
interface more general.
Let’s start with our new class MyParentClass
. We are using what we already learned with a small twist:
public class MyParentClass : IDisposable { private bool _disposed = false; private readonly ManagedResource _parentManagedResource; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { Console.WriteLine($"Called {nameof(MyParentClass)}.{nameof(Dispose)}"); if (_disposed) { return; } if (disposing) { _parentManagedResource.Dispose(); } _disposed = true; } }
Here, we move our disposing logic to a separate virtual method, also named Dispose()
, which child classes can override. That method has a parameter called disposing
. We will dispose of any managed resources by setting disposing
to true. Our new method will run when the public method calls it. By doing this, we successfully achieve the implementation of the IDisposable
interface.
Let’s now implement our child class:
public class MyChildClass : MyParentClass { private bool _disposed = false; private readonly ManagedResource _childManagedResource; protected override void Dispose(bool disposing) { Console.WriteLine($"Called {nameof(MyChildClass)}.{nameof(Dispose)}"); if (_disposed) { return; } if (disposing) { _childManagedResource.Dispose(); } _disposed = true; base.Dispose(disposing); } }
There are key points for us regarding inheritance and IDisposable
. First, we should override the inherited Dispose()
method in derived classes to dispose of the derived class’s specific resources.
Also, we should always call the base class’s Dispose()
method to ensure the proper release of all resources in the hierarchy.
Finally, we should set a private _disposed
field in base and derived classes to track if we have already disposed of the resource.
We call this pattern the Dispose Pattern. It ensures classes in hierarchies dispose of resources properly and efficiently, allowing each class to clean up its resources correctly without interfering with resource disposal in the base class.
Automatic Invocation of the IDisposable
We often ask if the system automatically calls the Dispose()
method. The answer is no.
While the .NET GC is efficient at managing memory, it does not automatically call the Dispose()
method of the IDisposable
interface. Therefore, we must explicitly call the Dispose()
method or use the using
statement for implicit disposal.
To prove that is the case, let’s review a couple of examples which we will add to our Program
class.
First, we will use an object of our class without calling the Dispose()
method or the using
statement:
Console.WriteLine("Dispose not called..."); var managedResource = new ManagedResource(); var myClass = new MyClass(managedResource); myClass.DoSomething();
Here, we create our managed resource, but we do not call Dispose()
method not explicitly nor implicitly.
Our results confirm this:
Dispose not called... Called MyClass.DoSomething
Now, let’s call the Dispose()
method explicitly:
Console.WriteLine("Dispose called explicitly..."); var managedResource = new ManagedResource(); var myClass = new MyClass(managedResource); myClass.DoSomething(); myClass.Dispose();
This time, after we call the DoSomething()
method, we make an explicit call to the Dispose()
method.
Now, seeing the results, we can confirm that the Dispose()
method was indeed called:
Dispose called explicitly... Called MyClass.DoSomething Called MyClass.Dispose Called ManagedResource.Dispose
Alternatively, we can use the using
statement:
Console.WriteLine("Dispose called implicitly..."); var managedResource = new ManagedResource(); using var myClass = new MyClass(managedResource); myClass.DoSomething();
In most cases, this is the preferred way of using disposable objects. Even though we are not calling Dispose()
method by ourselves, it can still be executed.
We can verify that it works and that Dispose()
is called:
Dispose called implicitly... Called MyClass.DoSomething Called MyClass.Dispose Called ManagedResource.Dispose
Finally, we will verify that our implementation works for the parent/child hierarchy:
Console.WriteLine("Dispose called for the whole hierarchy..."); var parentManagedResource = new ManagedResource(); var childManagedResource = new ManagedResource(); using var childClass = new MyChildClass(parentManagedResource, childManagedResource); childClass.DoSomething();
This time, we want to release all managed resources for parent and child classes. So the Dispose()
method should be executed multiple times, once for the childClass
then for its managed resource, and finally for the parent class and its managed resource.
Our results meet our expectations, showing that the Dispose()
method was called multiple times as expected:
Dispose called for the whole hierarchy... Called MyParentClass.DoSomething Called MyChildClass.Dispose Called ManagedResource.Dispose Called MyParentClass.Dispose Called ManagedResource.Dispose
Best Practices for IDisposable Interface
There are some best practices to consider when we want to correctly implement the IDisposable
interface.
We should dispose of IDisposable
objects as soon as we can. When using IDisposable
objects as instance fields, implementing the IDisposable
interface becomes necessary.
Additionally, allowing the Dispose()
method to be called multiple times without throwing exceptions is advisable. We should implement IDisposable
to support the disposal of resources in a class hierarchy.
Enabling static analysis with rule CA2000, which involves disposing of objects before losing scope, can be beneficial. Our understanding of the domain and tools is crucial; sometimes, disposing of objects on our own is not advisable, even if they implement the IDisposable
interface (such as with the HttClient class in ASP.NET Core).
We should declare a finalizer for cleanup when using unmanaged resources. If our class uses an async disposable field, we should implement the IAsyncDisposable interface.
Conclusion
Now we know how to correctly implement the IDisposable interface, which helps us with managing resources. This approach ensures proper resource release, preventing leaks and potential performance issues. We should remember that the GC does not automatically call the Dispose() method; thus, we must do so correctly. By following the guidelines, we can ensure that our applications run efficiently and reliably.