Why does Unity override the null comparison for Unity objects?

“Unity overrides the null comparison operator for Unity objects, which is incompatible with null propagation.”

Is there any workaround that allows null propagation with Unity objects?

This is bad:

This is OK:

Why ask why? Try Bud Dry!

Rivers of digital ink have been spilled over this topic, capturing the angst of Computer Science PHDs.

It all comes down to not being able to use newfangled .? and ?? operators.

That’s it. You are not prevented from doing anything, just using new syntactic sugar.

The tl;dr is that this is extremely unlikely to ever change because it would probably break 99% of all projects out there… and for what? Just to save lines of code? Lines of code are GOOD!

Rewrite your stuff using normal if / else statements. You’ll thank yourself when you end up with a bug and have to place breakpoints within your nice “aired out” code.

More: Nullable types (nullables):

While Unity games are written almost entirely using C#, Unity is actually a C++ engine with a (large) C# scripting layer. Every instance of a C# class which inherits from UnityEngine.Object has a reference to an internal C++ object.

These C++ objects happen have their lifetimes managed in a different way than their C# counterparts. The biggest different is that the C++ objects can be explicitly destroyed and are not subject to C#'s garbage collection. For example:

var mat = new Material();
DestroyImmediate(mat);
Debug.Log(mat is null); // This will print false
Debug.Log(mat == null); // This will print true

Line 1 creates a C# instance of the UnityEngine.Material class and a C++ instance of the C++ Material class.

Line 2 destroys the C++ Material instance, but it can’t do anything about the C# object referenced by the mat variable because there’s no such thing as explicit destruction in C#: reference types are only deallocated when they eventually are garbage collected. That’s why line 3 prints “false”: the variable mat still points to a non-null C# object.

Unity overrides the == operator on the UnityEngine.Object class so it can check if the C++ object referenced by the C# object is valid. This is why line 4 prints “true”.

This is an architectural decision that was made all the way back in Unity’s early years, and I assume it was made to make scripting less verbose and reduce friction. The more “standard” way to implement this would be to make UnityEngine.Object implement IDisposable, use .Dispose() instead of Destroy/DestroyImmediate, and use a property like .IsDisposed or similar for checking if objects are still valid to use. That would still not allow you to use null propagation safely, of course.

9 Likes

As for a work-around to not using using null operators on unity Objects you can use Ternary Operators:

_blockerSpriteRenderer.sprite = ActiveBlocker
   ? ActiveBlocker.CurrentSprite
   : null;

This is how I usually write my ternary operators, for readability, but its also valid as a one-liner if you like it being concise to a single line

_blockerSpriteRenderer.sprite = ActiveBlocker ? ActiveBlocker.CurrentSprite : null;

If you’re not familiar with ternary operators, its basically the one-line equivalent of an if/else statement.

code before the ‘?’ is a boolean expression (thus in this case ActiveBlocker is implicitly casted to a boolean using UnityObject’s boolean operator).
code between ‘?’ and ‘:’ is what is returned if the expression is true.
code between ‘:’ and ‘;’ is what is returned if the expression is false.

2 Likes

You can write an extension method that returns null when an UnityEngine.Object is null or destroyed, allowing to use null propagation and null coalescing operators :

/// <summary>
/// Return <c>null</c> when this <paramref name="unityObject"/> reference is <c>null</c> or destroyed, otherwise return the <paramref name="unityObject"/> instance

/// Allow using null conditional and null coalescing operators with <c>UnityEngine.Object</c> derivatives while conforming to the "a destroyed object is equal to null" Unity concept.

/// Example :

/// <c>float x = myUnityObject.DestroyedAsNull()?.myFloatField ?? 0f;</c>

/// will evaluate to <c>0f</c> when <c>myUnityObject</c> is destroyed, instead of returning the value still available on the managed instance.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T DestroyedAsNull<T>(this T unityObject) where T : UnityEngine.Object
{
    return unityObject ? unityObject : null;
}

Note that this won’t prevent the code analyzer to be unhappy, you will have to suppress the UNT0008 message.

As for the answer to why does Unity has those equality overrides, the answer is that this is a bad early design decision. See https://blog.unity.com/technology/custom-operator-should-we-keep-it

1 Like

Just to add: since the C++ and C# counterparts of an object can live/die independently of each other, there’s a couple interesting side effects that may catch you off guard:

  • You can still access fields and call methods on instances of your own Monobehaviour-derived classes after they were destroyed just fine because those live in the C# object which will remain in memory as long as there are any references to it. You’ll only get a MissingReferenceException when you try to access any native Unity property/method which relies on the C++ object (like .transform). This can lead to exceptions being thrown from slightly confusing places if you’re not aware of this.

  • Since the C++ objects aren’t destroyed by the C# garbage collector, you can actually leak memory if you’re not careful around certain classes. For example, if you create a Texture2D or Render texture and forget to destroy it.

3 Likes

Yes, this is true, but only partially. They are not actual “leaks” because every UnityEngine.Object derived class is tracked by the engine and can be found though FindObjectsOfType and similar methods. So seemingly loosing a reference to an object is not as bad as it would be in a native C++ application.

Also when calling Resources.UnloadUnusedAssets, Unity would actually destroy objects that are no longer in use. This happens automatically when you load a new scene, but I would highly recommend to pay attention to the objects you create (explicitly or implicitly) and clean them up when you actually don’t need them anymore. As you said, common types are textures, meshes, materials and essentially anything that is not visible in the hierarchy (because those would be visually noticed by the developer :slight_smile: )

2 Likes

Wow! Thanks for all the great responses. I had a feeling this had something to do with the “purgatory” that unity objects are in after calling Destroy(). I didn’t know about the C++ objects so that makes sense. I really appreciate all your time answering this.

3 Likes