1

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?

0

2 Answers 2

0

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

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);

Not the answer you're looking for? Browse other questions tagged or ask your own question.