Skip to content
HY Devlog
Go back

Fake Null on Unassigned Fields Only Happens in the Editor

I clipped this after hearing, while building a game project and studying as I went, that there’s such a thing as “fake null.” It’s the kind of post that comes up first when you search the term — a 2023 write-up of why fake null happens on UnityEngine.Object. It starts from the mismatched lifetimes of the native C++ object and its C# wrapper, then covers the == overload, the (object) cast, and ReferenceEquals. The explanation of the mechanism is accurate.

What snags is the “reducing the cost of the operation” list at the end. Five of its six items are off. It calls something cheaper that runs the exact same function, two of the snippets don’t compile, and the final “mix them” example rests on a premise that contradicts what the post itself explained earlier.

There’s also one word missing from the mechanism half — the “in the editor only” that Unity’s official blog attaches when it describes unassigned fields.

Table of Contents

Table of Contents

The Mechanism Is Described Correctly

Start with what’s right. The clipping’s summary:

C++ manages memory with pointers and C# has garbage collection manage the freeing, and fake null arises from that difference.

Unity’s official blog from 2014 says the same thing.

The lifetime of these c++ objects like GameObject and everything else that derives from UnityEngine.Object is explicitly managed … Lifetime of c# objects gets managed the c# way, with a garbage collector.

If you compare this object to null, our custom == operator will return “true” in this case, even though the actual c# variable is in reality not really null.

It’s also right that casting to (object) sidesteps the overload. The source makes the reason obvious: every UnityEngine.Object comparison funnels into one method, CompareBaseObjects.

public static bool operator==(Object x, Object y) { return CompareBaseObjects(x, y); }

public static bool operator!=(Object x, Object y) { return !CompareBaseObjects(x, y); }

static bool CompareBaseObjects(UnityEngine.Object lhs, UnityEngine.Object rhs)
{
    bool lhsNull = ((object)lhs) == null;
    bool rhsNull = ((object)rhs) == null;

    if (rhsNull && lhsNull) return true;

    if (rhsNull) return !IsNativeObjectAlive(lhs);
    if (lhsNull) return !IsNativeObjectAlive(rhs);

    return lhs.m_EntityId == rhs.m_EntityId;
}

IsNativeObjectAlive is the part that checks the native side, and that’s why it costs more than you’d think. The blog says as much.

Comparing two UnityEngine.Objects to eachother or to null is slower than you’d expect.

Fake Null on Unassigned Fields Is Editor-Only

The clipping writes:

Fake null also applies when a field is declared [SerializeField] and has never once been assigned in the Unity editor.

True — but one condition is missing. Here’s the blog’s wording.

When a MonoBehaviour has fields, in the editor only, we do not set those fields to ‘real null’, but to a ‘fake null’ object.

“In the editor only.” The reason is spelled out too.

Our custom == operator is able to check if something is one of these fake null objects, and behaves accordingly.

“looks like you are accessing a non initialised field in this MonoBehaviour over here, use the inspector to make the field point to something”

It’s a device for debugging messages. So it doesn’t go into a build.

Here’s how that difference shows up in practice.

Stateobj == nullReferenceEquals(obj, null)
Never assigned (Editor)truefalse (a fake null object is in there)
Never assigned (build)truetrue (real null)
Right after Destroytruefalse (the wrapper is alive)
A live objectfalsefalse

Rows one and two differ. A branch written with ReferenceEquals or an (object) cast behaves one way in the Editor and another in a build. The clipping does warn that “just because it’s faster, casting everything to System.Object is not advisable” — but it gives the reason as “a SerializeField’s null always comes in as fake null,” leaving out the half that says builds don’t work that way.

Going Through the “Reduce the Cost” List

Six items are listed. One at a time.

1. if (gameObject) Calls the Same Function as == null

The first item:

1. Use the implicit conversion to bool

if (gameObject)

It doesn’t reduce anything. The source is one line.

public static implicit operator bool(Object exists)
{
    return !CompareBaseObjects(exists, null);
}

It’s the very same function == calls — CompareBaseObjects(x, y) — with null passed in and the result negated. The native check happens all the same.

You can use it because it reads well. Not to make it cheaper.

2. gameObject = null Doesn’t Compile

The second item’s code:

Destroy(gameObject);
        gameObject = null;

MonoBehaviour’s gameObject is a read-only property inherited from Component. The source has no setter.

public extern GameObject gameObject
{
    [FreeFunction("GetGameObject", HasExplicitThis = true)]
    get;
}

Assigning to it is a compile error. The same problem shows up once more at the end of the list.

gameObject = gameObject ? gameObject : gameObject2;

The intent is right. Clearing a field you own to null after Destroy does make it genuinely null afterwards, which makes the check cheaper and the meaning clearer. That target just can’t be gameObject. It has to be a field like _target that you declared.

As an aside, that ternary isn’t ?. (null propagation) — it’s the conditional operator, and the condition position triggers the implicit bool conversion, so it does perform the Unity check properly. It’s grouped under “cheap options” but it’s a different animal.

3. ?? and ?. Aren’t a Shortcut — They’re a Different Check

Here’s how the clipping introduces ??:

Destroy(go1);

go1 ?? go2; // if go1 is null return go2, otherwise return go1 as is

The comment doesn’t match the behavior. After Destroy(go1), go1 is not really null. So ?? returns the destroyed go1, not go2. The blog nails this exact point.

It behaves inconsistently with the ?? operator, which also does a null check, but that one does a pure c# null check, and cannot be bypassed to call our custom null check.

The clipping’s explanation — “it isn’t an overloaded operator, so it doesn’t report fake null” — is technically correct. The problem is that it appears as an advantage in a “reduce the cost” list. You end up working with a destroyed object.

The Unity analyzers Microsoft ships flag these patterns as Correctness diagnostics.

RuleDescription
UNT0007Null coalescing on Unity objects (??)
UNT0008Null propagation on Unity objects (?.)
UNT0023Coalescing assignment on Unity objects (??=)
UNT0029Pattern matching with null on Unity objects

Correctness, not performance. I covered Unity putting ReferenceEquals at the top of its forbidden list in the code optimization post — same reason.

4. The “Mix Them” Example Has Its Premise Inverted

The last item:

if(ReferenceEquals(gameObject, null) && gameObject == false)
        {
        }

With this explanation attached:

if it’s in a Destroyed state, ReferenceEquals gives true, and the gameObject implicitly converted to bool comes out True

That’s wrong. In a destroyed state, ReferenceEquals(obj, null) is false. The C# wrapper is still alive — which is the definition of fake null the clipping itself gave earlier in the same post. The premise flipped mid-article.

The logic collapses too. Following CompareBaseObjects shows it immediately:

So whenever the first condition holds, the second always holds. The second check in that && filters nothing. The expression is identical to ReferenceEquals(gameObject, null) on its own.

What It Got Right: One-Sided Null Is the Expensive Case

The list is off, but the sentence right before it is accurate.

The most expensive operation is when only one side is null.

The branches in CompareBaseObjects are the evidence.

Only the one-sided-null branch touches the native side — and obj == null, the comparison we write most, is exactly that branch. The clipping states this without a source, and the source backs it up.

The other claim alongside it — “if(go == null) is more expensive than GetComponent” — I couldn’t verify. What the Unity blog says stops at “slower than you’d expect”; I found no statement comparing it to GetComponent. I won’t assert it either way.

Where and Why You’d Use It

Knowing about fake null actually simplifies the rule. On a UnityEngine.Object, use == null and nothing else.

== null Is the Whole Rule

using UnityEngine;

public class TargetTracker : MonoBehaviour
{
    [Header("Target")]
    [SerializeField, Tooltip("The target to follow")]
    private Transform _target;

    private void Update()
    {
        // The standard Unity check. Destroyed and unassigned both land here.
        if (_target == null)
        {
            return;
        }

        transform.LookAt(_target);
    }

    /// <summary>Destroys the target and clears our own reference.</summary>
    public void DestroyTarget()
    {
        if (_target == null)
        {
            return;
        }

        Destroy(_target.gameObject);

        // A field we declared can be cleared. From here on it's genuinely null.
        _target = null;
    }
}

_target = null is what the clipping’s second item was reaching for. It has to be a field of ours, not gameObject, to compile. And clearing it this way makes every later check a real null check — so the “cost reduction” it wanted falls out naturally.

The places you’d reach for ?? or ?. look like this instead.

// Wrong: a destroyed _target comes back
// Transform current = _target ?? _fallback;

// Right: goes through the Unity check
Transform current = _target != null ? _target : _fallback;

// Wrong: touches a destroyed object
// _target?.gameObject.SetActive(false);

// Right
if (_target != null) { _target.gameObject.SetActive(false); }

The one exception is plain C# objects. On a class that doesn’t derive from UnityEngine.Object, ?. and ?? are fine. Writing that distinction down makes it settle arguments in code review.

private PlayerInputSystem _input;      // plain C# object → ?. is fine
private Transform _target;             // UnityEngine.Object → ?. forbidden

private void OnDestroy()
{
    _input?.Dispose();                 // this one is correct
    // _target?.gameObject ...         // this one must not be used
}

Where Not to Use It

Wrapping Up

The half explaining fake null and the half talking about optimization read like two different articles. The first explains that == checks the native side for you; the second lists ways to skip that check as benefits. Skipping it is genuinely faster — and the reason it’s faster is the reason it’s wrong.


References

The starting point for this post was usingsystem — [Unity] 유니티 오브젝트 Fake Null과 Null 처리 (2023-07-13). I followed its explanation of the mechanism as written, then checked each item in the optimization list that follows against Unity’s official blog and the UnityCsReference source. Quotes from it are my translations.


Share this post:

Previous Post
Thousands of Players on Mirror? The Official Guide Says 200-300 Per Instance
Next Post
What You Pay For With Claude Code Isn't MCP