C#'s "is null" Considered Harmful

C#'s "is" operator allows a different kind of null checking, but in some cases, this may cause bugs that may be hard to debug.

Intro

Some years ago, C# 7 introduced the is operator for pattern matching.

The is operator for pattern matching also introduced an alternative null check, by using x is null instead of x == null. However, the x is null null check does not work the same way as x == null and there is a good reason why.

Difference between x == null and x is null

In most cases, you will not notice a difference between both usages:

var x = null as string; // or any other "normal" C# type

if (x is null) 
{
  // true
}

if (x == null)
{
  // true
}

However, it’s these edge cases that will cause hard to debug bugs.
Lets change the example above and use a special type instead:

var x = new IsNullExample();

if (x is null) 
{
  // false
}

if (x == null)
{
  // true
}

Wait, what just happened here?
Let’s take a look at the definition of the IsNullExample class:

class IsNullExample
{
    public static bool operator == (IsNullExample left, IsNullExample right)
    {
        if (Object.ReferenceEquals(right, null))
        {
           return true;
        }
        
        return Object.ReferenceEquals(left, right);
    }
    
    // This is mandatory if you overload the == operator
    public static bool operator!= (IsNullExample left, IsNullExample right)
    {
        return !(left == right);
    }
}

As you may have already guessed, using x is null does not invoke the == operator. This is why it exists in the first place.

According to the Microsoft docs for the is operator:

When you match an expression against null, the compiler guarantees that no user-overloaded == or != operator is invoked.

Now you may wonder why you or anyone else would ever want to override null checks for the == operator. Well, overloading == for custom null checks is actually often used when wrapping a native object with C#.

Let’s see how this is the case with the Unity engine.

Case study: Unity

Unity Objects such as GameObject, MonoBehavior or Component overload the == operator and return true if checking for null when the native counterpart got destroyed.

To understand how this is implemented, let’s inspect the Unity source code for Object:

namespace UnityEngine
{
    public partial class Object
    {
        // ...
        public static bool operator==(Object x, Object y) { return CompareBaseObjects(x, y); }
    
        public static bool operator!=(Object x, Object y) { return !CompareBaseObjects(x, y); }

        public override bool Equals(object other)
        {
            Object otherAsObject = other as Object;
            // A UnityEngine.Object can only be equal to another UnityEngine.Object - or null if it has been destroyed.
            // Make sure other is a UnityEngine.Object if "as Object" fails. The explicit "is" check is required since the == operator
            // in this class treats destroyed objects as equal to null
            if (otherAsObject == null && other != null && !(other is Object))
                return false;
            return CompareBaseObjects(this, otherAsObject);
        }
        // ...
    }
}

Here is an example scenario where x is null is used wrong:

var x = new GameObject();
UnityEngine.Object.DestroyImmediate(x);
// Calling DestroyImmediate will immediately destroy the native  
// counterpart and trigger Unity's custom null check logic 
// on the == operator.

if (x is not null) // or !Object.ReferenceEquals(x, null)
{
   // true (because for the .NET runtime, x is still alive) 
 
   // If you try to call any Unity API at this point using x, you will 
   // likely end up with weird exceptions or even crashes
}

if (x != null)
{
   // false  
 
   // We can safely call any Unity APIs here
}

So we may end up with x == null returning true even if the actual C# object reference may not be null.

C#‘s “is null” Considered Harmful

tl;dr:
x is null is actually just syntactic sugar for Object.ReferenceEquals(x, null). In contrast to x == null, it does not run any potential custom logic that checks if an object is null or not, which is a common case in scenarios where C# objects wrap native objects. Unity Objects are a good example for this.

For inexperienced developers, and especially for Unity developers, using x is null can lead to hard-to-debug bugs, such as weird exceptions or even crashes. I have seen several cases where developers have used x is null solely for reasons such as it “appealing more to the eye” or “to be consistent with other is pattern match checks”.

Because of this, I believe the x is null null check added more damage than value to the C# language. If anyone wanted to bypass user-overloaded == operators, they could have just used Object.ReferenceEquals instead, which, in contrast to x is null, clearly states it’s purpose.