In this article, we are going to learn about IDisposable objects and how to manage them in C#.
While developing applications in C# or any other programing language, we create objects to hold up our data. Upon creation, they allocate memory. After our application is finished using those objects, the memory they occupied is reclaimed back by the common language runtime’s garbage collector and it is available for reallocation. Whenever this is the case, we term our code as managed.
About IDisposable Objects
Sometimes, especially when we try to read a file, connect to a database or establish network connections, even though the garbage collector can track the lifetime of objects defined to hold these resources, our app doesn’t know how to clean them up.
The fact that we have such objects, makes our code unmanaged.
To demonstrate this, let’s create a C# Console app and update the Program
class :
public static void Main() { FileManager fileManager = new(); Console.WriteLine(fileManager.UnmanagedObjectFileManager()); }
Of course, we have to create the FileManager
class and add the UnmanagedObjectFileManager()
method:
using System.Globalization; namespace IDisposableObjects { public class FileManager { private readonly string _exampleFilePath; public FileManager() { _exampleFilePath = $"{Directory.GetParent(Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)) .Parent.Parent.Parent.FullName}/codeMazeExample.txt"; if (!File.Exists(_exampleFilePath)) { StreamWriter? exampleFileWriter = new(_exampleFilePath, append: true); exampleFileWriter.WriteLine("example File Content"); exampleFileWriter.Close(); } } public int UnmanagedObjectFileManager() { StreamReader? exampleFileReader = new(_exampleFilePath); string? exampleFileContents = exampleFileReader.ReadToEnd(); StringInfo? exampleFileReaderInfo = new(exampleFileContents); return exampleFileReaderInfo.LengthInTextElements; } } }
In the constructor, we get the base path, navigate to the projects directory and create the codeMazeExample.txt
file. Next, we add the UnmanagedObjectFileManager()
method, where we read the content of the created file.
As we can see, we have not employed any mechanism to handle the disposal despite having two disposable objects exampleFileReader
and exampleFileWriter
.
How to Manage IDisposable Objects
In C#, we have the IDisposable
interface available to extend whenever we have to manually clean up objects. Since it is not possible to predict when the garbage collection will be performed in the case of unmanaged code, we have the Dispose()
method available from the IDisposable
interface, which we can use in our code to clean IDispossable
objects.
There are typically two ways to manage IDispossable
objects in C#:
- The
using
statement or declaration - Try/finally block
The Using Declaration
By using the using
statement or declaration, it disposes of the IDispossable
object(s) automatically without the need to explicitly call the Dispose()
method.
Let’s update our code in FileManager
class constructor:
public FileManager() { ... if (!File.Exists(_exampleFilePath)) { using StreamWriter? exampleFileWriter = new(_exampleFilePath, append: true); ... } }
In the constructor, we update the exampleFileWriter
by adding the using
keyword at the beginning to manage its disposal.
Then, we are going to add a new UsingFileManager()
method that uses the StreamReader object properly:
public int UsingFileManager() { using StreamReader? exampleFileReader = new(_exampleFilePath); string? exampleFileContents = exampleFileReader.ReadToEnd(); StringInfo? exampleFileReaderInfo = new(exampleFileContents); return exampleFileReaderInfo.LengthInTextElements; }
We add using
declaratively on exampleFileReader
to handle its disposal as well.
Finally, we can call this method from the Program
class’s Main()
method:
public static void Main() { ... //Console.WriteLine(fileManager.UnmanagedObjectFileManager()); Console.WriteLine(fileManager.UsingFileManager()); }
Try/Finally Block
When we use the Try/Finally block, we explicitly have to call the Dispose()
method on our disposable objects.
Let’s further update the code in the Program
class’s Main()
method:
public static void Main() { ... //Console.WriteLine(fileManager.UsingFileManager()); Console.WriteLine(fileManager.TryFinallyFileManager()); }
As expected, we have to add the TryFinallyFileManager()
method in the FileManager
class:
public int TryFinallyFileManager() { int exampleFileReaderInfoLength = 0; StreamReader? exampleFileReader = null; try { exampleFileReader = new(_exampleFilePath); string? exampleFileContents = exampleFileReader.ReadToEnd(); exampleFileReaderInfoLength = new StringInfo(exampleFileContents).LengthInTextElements; } catch (FileNotFoundException) { Console.WriteLine("The file cannot be found."); } catch (IOException) { Console.WriteLine("An I/O error has occurred."); } catch (OutOfMemoryException) { Console.WriteLine("There is insufficient memory to read the file."); } catch (Exception ex) { //handle any other exception } finally { exampleFileReader?.Dispose(); } return exampleFileReaderInfoLength; }
Here, we use try/catch/finally to handle any errors that may arise and dispose of exampleFileReader
in the finally
block.
Try-Finally Without a Catch Block
We have to be very careful if we try to use the previous example but without all the catch blocks or that null conditional (?
) operator. The biggest issue with this approach is if any exception occurs, our application will break. So at least that final catch
block is always good to have.
Also, without the null conditional ?
operator (as many don’t use it all the time), an exception may occur if, for some reason, our StreamReader
object is null once we try to dispose of the StreamReader
object.
So, what we can do in this case is use try/finally
block, but with an additional check in the finally
block:
public int TryFinallyFileManager() { ... finally { if (exampleFileReader != null) { exampleFileReader.Dispose(); } } return exampleFileReaderInfoLength; }
Here, in the finally
block, we explicitly check to make sure that the exampleFileReader
is not null and then dispose of it. It may be a more readable way of doing that null check, especially if we want to add some custom logic if the object is null.
Of course, if we want to do something like this, maybe the better solution is just to go with the using
statement as it does the same check behind the scenes.
When to Use Each of the Two Ways
Although both ways are valid in managing IDisposable
objects, which one to use depends on the use case at hand. For instance, if we are sure that the objects we are using in our code are always available and that our resources are inexhaustible, we can employ the using
statement since we would be sure our code will throw no exceptions.
In most cases, if not all, it’s very hard to be certain of the environment in the statement above, it’s always important to handle uncertainties that may arise during the course of our application lifetime. In this scenario, it is best practice to use try/catch/finally
to gracefully handle exceptions that may occur.
Conclusion
In this article, we learned how to dispose of disposable objects in C#. Disposing of IDispossable
objects is a good practice whenever we interact with them. Given that most of the time we do not get any exceptions if we do not clean these objects, if not disposed it could lead to memory leakages or out-of-memory problems or both leading to unnecessary app crashes.