How can IDisposable field be null here?

Viewed 206

I've a lot of examples that a disposable class disposes its disposable field with null-conditional operator like _field?.Dispose(). And Resharper also generates code with a null check.

class Foo : IDisposable
{
    private readonly Bar _bar;

    public Foo()
    {
        _bar = new Bar();
    }
    
    public void Dispose()
    {
        _bar?.Dispose();
    }
}

class Bar : IDisposable
{
    public void Dispose()
    {
    }
}

My question is why the null check on field _bar is used here? It can not be null at this point. It is not garbage collected unless the containing object is garbage collected because it holds a reference to it. And if containing object i.e. Foo object was null, we can never call .Dispose() on it because it will throw a null reference exception.

2 Answers

You're, right, _bar can't be null in this particular example. It's a combination of a few factors:

  • There is only one constructor which always initializes a Bar object
  • _bar is readonly, which guarantees that no one can set it to null afterwards

But it's not hard to come up with an example where it could be null (e.g. changing any of the above bullet points). Even for this simplistic code example, I'd already argue that Bar most likely should be injected if you want this to be a clean codebase, which throws out your argument that you can guarantee that _bar won't be null.

What would be the benefit of specifically creating both a null-protecting and a non-null protecting variation of the code, as opposed to just always protecting against null? It takes more effort, you risk choosing the wrong option; and what's the benefit? Nothing.

So if there's no benefit to intentionally omitting the null protection, why bother figuring out if it's necessary to protect against null? It's easier to just include the null protection than it is to take time and figure out if you might not need null protection in this particular case.

Note also that you're specifically referring to either demo code online or templates generated by Resharper, which in both cases are intended to reach a wide audience and remain widely applicable.

Whilst the other answer is correct for the purposes of most "normal" usage of a type, there are at least two slightly more unusual scenarios where _bar could be null in Dispose() despite the appearing to always be initialized via the sole constructor.

It's worth understanding that the creation of an object in .NET is essentially a 2 step process:

  1. Allocate the object
  2. Call the appropriate constructor

There are a number of ways that step 1 can occur without step 2, which leaves you with an object for which all the fields are in the default state for the respective type (e.g. null for reference types, zero for numeric types, etc.).

The first is intentionally via FormatterServices.GetUninitializedObject(), which is primarily used by code intended to deserialize an object (e.g. after having been persisted to a file). As the name perhaps suggests, it allocates an instance of an object (step 1), but does not execute the constructor. For your example Foo class, this would result in _bar remaining null. If Dispose() were then to be called on the instance without having had _bar initialized by some other means (e.g. reflection), then a null reference exception would be thrown in the Dispose() method without the null check on _bar.

The other even less common possibility results from a long-standing (almost 5 year old) bug in .NET itself. Whilst it won't occur on the exact code in your example, a slight modification (the introduction of a Finalizer) makes it possible.

Here's an example:

class Foo : IDisposable
{
    private readonly Bar _bar;

    // The constructor needs to have at least one argument
    public Foo(string someArg)
    {
        _bar = new Bar();
    }

    public void Dispose()
    {
        Dispose(true);
    }

    // And we need a finalizer
    ~Foo()
    {
        Dispose(false);
    }

    private void Dispose(bool disposing)
    {
        Console.WriteLine("{0} and _bar is {1}", disposing ? "Disposing" : "Finalizing", _bar == null ? "null" : "not null");
    }
}

And to trigger it:

static void Main(string[] args)
{
    try
    {
        // We intentionally trigger an exception when getting the arg to pass to `Foo()`
        // to trigger the bug, however in real-life example you might call a method
        // here that sometimes throws
        using (var foo = new Foo(args[-1]))
        {
            // ...
        }
    }
    catch
    {
    }

    // These two lines aren't required for the bug to be triggered,
    // they simply allow us to see it without waiting for a GC.
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

The output from this when I run it is:

Finalizing and _bar is null

The explanation is relatively straightforward and again results from the two step process of creating an object. One might expect that .NET would always perform step #2 immediately after step #1, but in fact it places the code that determines the values of the constructor arguments between the two steps, if any of that code throws an exception as we (intentionally) do here, then step #2 is skipped. Whilst we can't access the object that was allocated (because it never gets assigned to the foo variable), because it's finalizable, .NET still keeps track of it in order to call its finalizer later.

Whilst both of these might seem somewhat hypothetical examples, I have encountered both of them in production code bases over the years, so worth being aware of!

Related