Unity lies to you about null

Oliver Oliver • Published 2 years ago Updated 2 years ago


What is the difference between component == null, component is null, and !component?

I recently came across somebody who asked this very question. Simply, what is the difference between the following conditions?

if (component == null)
if (component is null)
if (!component)

In C# 6 and earlier, foo == null was the most common way to check if an object reference is null. However, this comes with a caveat!

Hijacking the == operator

C# allows us to overload various operators, and == is one of them. To show what I mean, let's take a very simple console application where I create a class with an overloaded == which invariably returns true.

static void Main()
{
    var instance = new MyClass();
    Console.WriteLine(instance == null);
}

class MyClass
{
    <mark>public static bool operator ==(MyClass left, MyClass right) => true;</mark>
    
    // this isn't being called, but we need to implement !=
    // when implementing == otherwise the compiler complains
    public static bool operator !=(MyClass left, MyClass right) => true;
}

I create an instance of MyClass, caching it in a variable named instance. Without doing anything else, I then output the result of instance == null.

The catch is that I have defined the == operator to return true regardless of its inputs. It doesn't care that I pass the same instance, a disparate instance, or null - it will simply return true. Therefore the output to the console is “True”.

If we take a look at the compiled result, we can see that it calls the op_Equality method - which is the operator we defined in MyClass.

newobj       instance void MyClass::.ctor()
ldnull
<mark>call         bool MyClass::op_Equality(class MyClass, class MyClass)</mark>
call         void [System.Console]System.Console::WriteLine(bool)

The workaround until now

Until C# 7, the workaround to this was to use the ReferenceEquals method. This method is defined for the base Object type, and is a static method which takes two parameters.

The way this method works is instead of doing a value comparison, it compares the actual references. If the two references point to the same location in memory, it returns true. Otherwise, it returns false.

So the way to do a real null check was:

var instance = new MyClass();
bool isNull = ReferenceEquals(instance, null);

Pattern matching to the rescue

Starting with C# 7, there's now another (safer) way to perform a null check in the form of the is operator. This operator compiles differently. Instead of calling the == operator, it actually compiles to a simple ceq (compare if equal) instruction.

static void Main()
{
    var instance = new MyClass();
    Console.WriteLine(instance <mark>is</mark> null);
}
newobj       instance void MyClass::.ctor()
ldnull
<mark>ceq</mark>
call         void [System.Console]System.Console::WriteLine(bool)

Because of this, the is operator is the preferred way to perform a null check. It bypasses any custom implementation that a type offers, and the .NET runtime compares the actual reference instead - this is what we want!

... Most of the time.

What does this have to do with Unity?

Now let's take a look at a simple behaviour in Unity.

using UnityEngine;

public class TestBehaviour : MonoBehaviour
{
    private GameObject _gameObject;

    private void Start()
    {
        _gameObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            DestroyImmediate(_gameObject);
            Debug.Log(_gameObject == null);
        }
    }
}

We create a cube in Start. After I hit space, we call DestroyImmediate on the game object, and output the result of _gameObject == null. What would you expect the output to be?

If one has C# experience but is new to Unity, one might wrongly assume that it returns false. After all, _gameObject still very much exists in memory and we haven't reassigned it to null anywhere. It must still contain a reference to a location in memory, right?

I find myself using this meme to answer questions more often than I care to admit.

But you, attentive reader, may already know what's going to happen.

Unity overloads the == operator

All types which inherit UnityEngine.Object (which essentially means almost everything in Unity) overload the == operator. This is done so that Unity can perform a lifetime check on the object. What this means is that before checking if the two components are considered equal, it actually checks if you've passed null. If you have, it then checks if that object has been destroyed! The output, therefore, is true.

A false positive.

Unity lies to you and tells you that the object is null, much like the custom MyClass from above, even though the location pointed at by _gameObject has not changed. If we were to add another log which uses the is operator, we can see this in action.

private void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
    {
        DestroyImmediate(_gameObject);
        Debug.Log(_gameObject == null);
        Debug.Log(_gameObject is null);
    }
}

The is operator - by design - bypasses Unity's check, and compares the actual references passed in. And so, the output is false.

A false positive, and a true negative.

The catch is, this isn't what we want! After all, this is how we check if objects have been destroyed. 99.9% of the time, we don't care about the reference itself - we want to know if Destroy has been called on it at some point.

This means that is should not be used with any type that inherits UnityEngine.Object. Consider having the following code:

DestroyImmediate(_gameObject);

if (!(_gameObject is null))
{
    Debug.Log(_gameObject.transform.position);
}

We destroy the object, and then check its reference isn't null. If it's not null, we try to output its position. Since is bypasses Unity's lifetime check, it returns true (the object is still allocated after all), but then immediately hit an exception because in Unity's eyes, the object no longer exists! We're attempting to access the position of a destroyed object.

You deceitful so and so.

Null-coalescing (??) and null-conditional (?.) operators

Because of this quirk about how Unity fakes null-ness, it means that we're not able to take advantage of the various null operators that C# has to offer. In a usual case, we would be able to use the null-conditional (?.) operator to conditionally access members only if the reference is not null. However, if we were to try this on a GameObject:

DestroyImmediate(_gameObject);
Debug.Log(_gameObject<mark>?.</mark>transform.position);

We'd hit the exact same exception. The .NET runtime determined that it was, in fact, not null - and proceeded to access transform. But Unity steps in and we hit an exception, because Unity determined that it was “null”.

The same applies to the null-coalescing (??) operator.

DestroyImmediate(_gameObject);
_gameObject <mark>??=</mark> GameObject.CreatePrimitive(PrimitiveType.Sphere);

Here, I destroy the object. Then I use the null-coalesecing assignment operator (??=) so that if the object is null, I create a new sphere instead.

But running this code, nothing happens. The cube indeed gets destroyed, but no sphere is created. This happens for the same reason - .NET determined that _gameObject was not null, and skipped the assignment. The documentation for UnityEngine.Object actually subtly mentions this little fact:

In the Small Printâ„¢.

Though they didn't bother to go into detail about why this is the case. It's also a bit misleading; it does “support” it. The operators work exactly as intended - bypassing custom == implementations - it's just that Unity hijacked the meaning of null and will give you results you weren't expecting.

Implicit bool conversions

Unity, however, did create a workaround in the past which may have benefited them today (depending on your definition of “benefit”).

UnityEngine.Object supports an implicit conversion to bool. The only rationale I can justify this being the case, is that the developers at Unity wanted null-checking to be similar to that of C/C++.

MyClass* ptr = nullptr; // or NULL if you are in C

if (!ptr) {
    // ptr is null
}

In C/C++, NULL (or nullptr) is essentially 0. Evaluating !ptr is basically asking "is ptr equal to zero?", which - in this case - it is.

Unity decided that it would be smart to have this sort of syntax when it comes to UnityEngine.Object. During the conversion, it performs the standard Unity lifetime check and functions the same as == null.

DestroyImmediate(_gameObject);
if (!_gameObject)
{
    Debug.Log("The object was destroyed");
}

C++#

The downside to this is that it obfuscates the true type. If we weren't using a variable named _gameObject but instead groundCheck, and you write if (!groundCheck), it wouldn't be immediately apparent what the type is. It could be bool, but it could also just as likely be a Collider.

This also has an inherent issue with beginners who learn C# through Unity. It is very possible for them to become accustomed to performing a null check this way. When they then come to try it outside of Unity (or with any type that isn't UnityEngine.Object), they are confused at an error such as:

error CS0023: Operator '!' cannot be applied to operand of type 'string'

Closing statement

As you may already be aware, I have deep resentment for Unity. They are persistently breaking C# guidelines any way they possibly can. Fake null, implicit bool conversions, everything is camelCase - WHY IS EVERYTHING CAMELCASE?

Alas, here we are. Unity has its issues, but as long as you're aware of the things it does… differently… we can survive this hell.