Difference in null behaviour between public/[SerializeField]private and private UnityEngine.Object

Hey all, relatively new to Unity so I apologize if this is common knowledge, but I was unable to find anything on this specific topic.

I was attempting to do an inline null check for a public Rigidbody2D component before utilizing its velocity on another component, the simplified version looks essentially like this:

move += rigidBody2D is null ? new Vector2(0, player.velocity.y) : new Vector2(rigidBody2D.velocity.x, 0);

Strangely, I found when running tests for this, if rigidBody2D wasn’t assigned a value, it was throwing an error instead of evaluating true at the null check and continuing appropriately. I did some poking around and ultimately disabled everything but a single GameObject in my project and threw this short test on it. It’s worth noting that throughout this process, I purposely never assigned anything to the public references (or the [SerializeField]private references further down).

using UnityEngine;

public class UnityObjectPublicPrivateNullCheck : MonoBehaviour
{
    private UnityEngine.Object privateUnityObjectNullCheck;
    public UnityEngine.Object publicUnityObjectNullCheck;
    private object privateSystemObjectNullCheck;
    public object publicSystemObjectNullCheck;

    void Start()
    {
        //null check on Unity object type
        Debug.Log(privateUnityObjectNullCheck ?? true); //returns true, expected
        Debug.Log(publicUnityObjectNullCheck ?? true); //returns false, huh???

        //null check on System object type
        Debug.Log(privateSystemObjectNullCheck ?? true); //returns true, expected
        Debug.Log(publicSystemObjectNullCheck ?? true); //returns true, expected
    }
}

This effectively gets my point across, but I did some further testing and found that public UnityEngine.Object are not actually returning null, but "null", whereas private System.Object, public System.Object, and private UnityEngine.Object all return null, expectedly.

One more additional test, I wagered a guess that this might have something to do with Unity’s serialization for the inspector, so I went ahead and put this line in the script:

[SerializeField]private UnityEngine.Object serializedPrivateUnityObjectNullCheck;
...
//null check on Serialized private Unity object type
Debug.Log(serializedPrivateUnityObjectNullCheck ?? true); //returns false, huh???

Finding the culprit, I’m still a bit confused. Surely, this is this intended behaviour, but why? Am I destined to have to check for UnityEngine.Object.ToString() == "null" for public and [SerializeField] private or is there a more efficient workaround?

Final note: I am running Unity 2020.1.17f1, not sure that matters.

Shouldnt
move += rigidBody2D is null ? new Vector2(0, player.velocity.y) : new Vector2(rigidBody2D.velocity.x, 0);

be

move += rigidBody2D == null ? new Vector2(0, player.velocity.y) : new Vector2(rigidBody2D.velocity.x, 0);


Also no there isn’t anything in the access modifier that would change the way null coalescing works what there is is that unitys editor adds a caching layer on-top of public fields. i.e. check what the value is in the inspector

I was directed to a duplicate of this question on StackOverflow for anyone curious about this. Turns out, Unity has an overriden implementation of Equals and null, this link covers it and why in more depth. The solution is simply dropping the null comparison in favor of a boolean comparison when dealing with UnityEngine.Object.