Menu

"is null" versus "== null" in C#

Introduction

Recently, the topic of using someObject is null over someObject == null has been coming up, and I'd like to give my two cents on the issue - in particular, I'd like to make the case for using is null in every case where you would previously use == null.

Tony Hoare, the inventor of the null pointer we all love and hate, has called null his "Billion Dollar Mistake", and some languages are now moving away from the wild west use of null that we have today. Kotlin and Rust are two of the more known ones, and C# is about to follow suit.

Accordingly, null is becoming more and more of a strictly exception value, and this is one of the reasons I'd like to advocate for is null over == null.

On the surface, these seem to be just two ways of expressing the same thing in C# - determining if an instance of an object is null - that is, it has no valid value.

It's at this point some of you are going to be getting ready to furiously type away - they're not the same, and you are absolutely correct. It's because of this reason that == null can lie to you.

Let me show you. Here's a piece of completely innocent-looking code:

var innocent = new Innocent();
if (innocent == null)
{
    Console.WriteLine(innocent.Laugh());
}

At first glance, the WriteLine ought to be unreachable - innocent is quite provably non-null. However, if the code is executed...

// Output: Mouahahaha!

How is this possible? Running some tests against the innocent instance produces some rather worrying - and confusing! - results.

Innocent nullInnocent = null;
Innocent anotherNullInnocent = null;

var isNullNull = nullInnocent == anotherNullInnocent;

var isInnocentNull = innocent == null;
var isNullInnocent = null == innocent;

var isInnocentInnocent = innocent == innocent;

Console.WriteLine($"{isNullNull}, {isInnocentNull}, {isNullInnocent}, {isInnocentInnocent}");

// Output: False, True, True, True

bwuh?!

At this point, it's quite evident that I'm pulling your leg - and you're correct! The Innocent class has an overridden == operator, which is causing this blatantly wrong behaviour:

public static bool operator ==(Innocent a, Innocent b)
{
    var eitherIsActuallyNull = ReferenceEquals(a, null) ^ ReferenceEquals(b, null);
    var bothAreActuallyNull = ReferenceEquals(a, null) && ReferenceEquals(b, null);

    if (eitherIsActuallyNull)
    {
        return true;
    }

    if (bothAreActuallyNull)
    {
        return false;
    }

    return ReferenceEquals(a, b);
}

In and of itself, this isn't anything special, and it's fairly obvious once you think about it. However, I'm going to take our shared little rabbit hole and dig it a little deeper. If we change one little thing about our initial example, things become much more interesting.

var innocent = new Innocent();
if (innocent is null)
{
    Console.WriteLine(innocent.Laugh());
}

// Output: 

Suddenly, everything is right with the world - innocent is not null, the block doesn't execute, nothing is written to the console. But how? The check should be the same. Everything changes now that we're using pattern matching instead, and this stems from the way pattern matching works under the hood. Instead of testing the contents of objects, we test on the shape of objects, and then extract information from them. null, being the exceptional value that it is, is considered a shape of its own and as such is treated accordingly.

Let's take a look at how the two are implemented in IL.

Implementation

So, what we are looking for here is a way to compare a given object (a class, which is a reference type) to a null, that is, a null pointer - an invalid object.

Equality Operator

The equality operator, which we are all familiar with, is the traditional way of testing if two values are equal to each other. However, the C# language (and many others) allows a developer to override the behaviour of the operator, giving it new meaning. This is extremely useful, and a near-requirement for structs, but does allow for nasty behaviour like the things outlined above.

private static bool NullPattern(MyClass obj)
{
    return obj == null;
}
.method private hidebysig static bool  MyClassNullEquality(class Scratchpad.MyClass obj) cil managed
{
    .maxstack  2
    IL_0001:  ldarg.0
    IL_0002:  ldnull
    IL_0003:  call       bool Scratchpad.MyClass::op_Equality(class Scratchpad.MyClass, class Scratchpad.MyClass)
    IL_000c:  ret
}

As we can see, the comparison to null is done by loading the two values (the argument, and the null value) on the stack, and then calling the overridden equality operator on the two values.

This way of checking doesn't neccesarily give us accurate results, as we've seen. On top of that, since it's a full-on method call which can contain arbitrary logic, the call may incur some overhead.

One particularly well-known example of this is the way Unity overrides the operator. By their own admission, comparisons of UnityEngine.Object types to null is slower than normal because of this, and it becomes inconsistent when used with the null-coalescing operator (which does the same thing as the pattern matching does!).

Pattern Matching

Pattern matching, on the other hand, is much simpler.

private static bool NullPattern(MyClass obj)
{
    return obj is null;
}
.method private hidebysig static bool  MyClassNullPattern(class Scratchpad.MyClass obj) cil managed
{
    .maxstack  2
    IL_0001:  ldarg.0
    IL_0002:  ldnull
    IL_0003:  ceq
    IL_0009:  ret
}

As before, we load the two values to be compared onto the stack, but this time there are no method calls involved. Instead, it's just a straight no-fuss value comparison - the pointer to the value, and a pointer to null. We check if they're literally equal and push the result onto the stack.

The equality check is now also a simple bit-by-bit comparison of two pointers, and there are no method calls or custom logic steps involved. Less code, less instructions, less overhead.

So what?

The end result of this little song and dance should, hopefully, be a better understanding of what goes into one of the most basic tools in the C# programmer's toolbox, and the ways you can use it to either get work done, play pranks, or make your code harder to maintain.

All this boils down to something I'd like to see more codebases adopt - usage of is null over == null. Primarily, it removes any form of ambiguity from your code - something either is or is not null, no inbetweens or special cases. Furthermore, comparing to null with the is null construct acts to separate null out from the rest of the code, and semantically elevate it to the exceptional value it should always have been.

On top of this, there might be a performance benefit if all you want to do is check if something is null - literal, actual null.

Sometimes, the need for an overloaded equality operator exists. I've used plenty of them myself, and they are the backbone of using reference types while still comparing the contained values inside of them. However, it is my firm opinion that null should stay far, far away from these types of overrides.

Something to think about, eh?