In this article, we are going to learn about IDisposable objects and how to manage them in C#.

To download the source code for this article, you can visit our GitHub repository.

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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!