Why is null Unity Object equal to null in specialized class but not equal to null in generic class?

38 views Asked by At

Attach TestMono to an empty GameObject, leave the serialized field uov empty and play. The result is not what I expected.

using System;
using UnityEngine;
using Object = UnityEngine.Object;

namespace TestMisc.Scripts
{
    [Serializable]
    public class Variable<T>
    {
        [SerializeField] T m_Value;

        public T Value => m_Value;

        public void LogInsideGenericClass()
        {
// Result: Inside generic class: equal to null: False    equal to default: True
            Debug.Log($"Inside generic class: equal to null: {Value == null}    equal to default: {Value as Object == default}");
        }
    }

    [Serializable]
    public class UnityObjectVariable : Variable<Object>
    {
        public void LogInsideSpecializedClass()
        {
// Result: Inside specialized class: equal to null: True    equal to default: True
            Debug.Log($"Inside specialized class: equal to null: {Value == null}    equal to default: {Value as Object == default}");
        }
    }
}
using UnityEngine;

namespace TestMisc.Scripts
{
    public class TestMono : MonoBehaviour
    {
        [SerializeField] UnityObjectVariable uov;

        void Start()
        {
            uov.LogInsideGenericClass();
            uov.LogInsideSpecializedClass();
        }
    }
}

Why null Unity Object is not equal to null inside generic class?

2

There are 2 answers

0
Verpous On

Unity override the == operator for Unity objects to make them equal to null in cases where the object isn't actually null (for example if it has been destroyed). Operators are not polymorphic; if you cast the object to Object then compare it with == it will use Object's == operator, not some "override" from an inheriting class.

If you compare using Equals it should yield the same result with or without a cast.

If you only want to check for truly null references, use ReferenceEquals.

0
derHugo On

In general see Unity custom == operator

=> it's a bit more complex.

Your generic == happens on a c# System.Object level - where a serialzed field in Unity is always initialized - just with an invalid UnityEngine.Object instance.

On the other side UnityEngine.Object == is in the end a pointer into the underlying c++ engine where those instances are actually living and maintained.

See source code

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_InstanceID == rhs.m_InstanceID;
}

static bool IsNativeObjectAlive(UnityEngine.Object o)
{
    if (o.GetCachedPtr() != IntPtr.Zero)
        return true;

    //Ressurection of assets is complicated.
    //For almost all cases, if you have a c# wrapper for an asset like a material,
    //if the material gets moved, or deleted, and later placed back, the persistentmanager
    //will ensure it will come back with the same instanceid.
    //in this case, we want the old c# wrapper to still "work".
    //we only support this behaviour in the editor, even though there
    //are some cases in the player where this could happen too. (when unloading things from assetbundles)
    //supporting this makes all operator== slow though, so we decided to not support it in the player.
    //
    //we have an exception for assets that "are" a c# object, like a MonoBehaviour in a prefab, and a ScriptableObject.
    //in this case, the asset "is" the c# object,  and you cannot actually pretend
    //the old wrapper points to the new c# object. this is why we make an exception in the operator==
    //for this case. If we had a c# wrapper to a persistent monobehaviour, and that one gets
    //destroyed, and placed back with the same instanceID,  we still will say that the old
    //c# object is null.
    if (o is MonoBehaviour || o is ScriptableObject)
        return false;
    
     return DoesObjectWithInstanceIDExist(o.GetInstanceID());
 }

 [NativeMethod(Name = "UnityEngineObjectBindings::DoesObjectWithInstanceIDExist", IsFreeFunction = true, IsThreadSafe = true)]
 internal extern static bool DoesObjectWithInstanceIDExist(int instanceID);