21

I've noticed that if I have some variables exposed to the Unity inspector such as:

[SerializeField] GameObject _tickIcon;

If I leave them unassigned and try to use the null conditional operator and call a method on that object I get an exception saying the variable is not assigned. So basically instead of doing this:

_tickIcon?.SetActive(false);

It's forcing me to do this:

if(_tickIcon != null)
{
   _tickIcon.SetActive(false)
}

So I'm guessing this must be something specific to unity's runtime, it's not really null, but I can check for null and it work. I don't really understand this.

user2000950
  • 511
  • 5
  • 14

3 Answers3

28

It does not work in general with anything inheriting from UnityEngine.Object!

tl;dr: The ?? and ?. operators work on a System.Object (aca object) level while Unity's == operator works on a UnityEngine.Object level.


It is bypassed due to how they internally implemented the ==/!= method differently. See Custom == operator, should we keep it?

ReSharper explained it pretty well in Possible unintended bypass of lifetime check of underlying Unity engine object

This warning is shown if a type deriving from UnityEngine.Object uses either the null coalescing (??) or null propagation or conditional (?.) operators. These operators do not use the custom equality operators declared on UnityEngine.Object, and so bypass a check to see if the underlying native Unity engine object has been destroyed. An explicit null or boolean comparison, or a call to System.Object.ReferenceEquals() is preferred in order to clarify intent.

UnityEngine.Object is in some occasions not really null but still keeps some meta data. So the underlying object(= System.Object) is not null, UnityEngine.Object's overwritten == operator just returns true for == null.

The reason for this: The c# layer UnityEngine.Object is just the developer API layer on top of the actual underlying c++ engine code. The custom == and implicit bool operator both basically boil down to

(source code)

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

background calls into the native engine code where the actual instances of those objcts are handled and their lifecycle controlled. So e.g. after Destroy an object, in the native c++ engine the object is already marked as destroyed and the custom == and bool operator already return false. The c# layer UnityEngine.Object still exists though until it is actually garbage collected (usually at the end of the frame but there is no real guarantee for that either).

The main reason why therefore things like _tickIcon?.gameObjct throw a NullReferenceException is that the ?. operator only directly works on the underlying object(System.Object) while the UnityEngine.Object works with their custom implementation on a different level.

E.g. after

Destroy(_tickIcon);
_tickIcon.SetActive(false);

you will note that you don't get a normal NullReferenceException which would be the case if it were actually null but rather get a Unity customs MissingReferenceException telling you a probable reason for why the exception was thrown.


Long story short: As solution UnityEngine.Object has the implicit bool operator

Does the object exist?

You should always check the existence of anything derived from UnityEngine.Object like this:

if(_tickIcon)
{
    _tickIcon.SetActive(false);
}

or explicit (as this will use the custom ==/!= operators)

if(_tickIcon != null)
{
    _tickIcon.SetActive(false);
}
derHugo
  • 83,094
  • 9
  • 75
  • 115
  • This is exactly the information I was looking for. I was suspecting they had some kind of operator specific implementation but I was not sure. Thanks! – user2000950 Jul 01 '20 at 15:09
  • So optional chaining can't be used for Unity objects? Not allowing a feature of the c# syntax for unity objects is not beneficial to most devs I would think. – Logan Cundiff Oct 11 '22 at 22:48
  • @LoganCundiff yes it can be used .. but it might misbehave ;) Well people complain a lot about Unity and the way things are implemented ... but you forget that actually Unity is all `c++`. The `c#` you are using is just a development layer on top of it to make your live easier .. so I would say it is a tradeoff between still making it work but easy to use .. with some exceptions about some `c#` features ;) The `?` and `??` are basically just shorthands for explicit `null` checks ... you can still use explicit null check if you don't like the implicit bool .. I got used to it – derHugo Oct 12 '22 at 07:03
  • Thanks for the info! Do you know of a situation in which `?` works with `UnityEngine.Object`? – Logan Cundiff Oct 12 '22 at 17:32
  • 1
    @LoganCundiff once the Garbage Collector already collected the object ..then it doesn't make a difference anymore since then the c# instance will actually be a `null` on `System.Object` level ... but there is no reliable time when the GC collects something (except explicitly forcing it) – derHugo Oct 20 '22 at 07:26
0

Just to remix the syntax of this question, when attempting something like this:

//Error
Vector3 targetPos = target?.position ?? Vector3.zero;

Instead try this:

//OK
Vector3 targetPos = target ? target.position : Vector3.zero;
chantey
  • 4,252
  • 1
  • 35
  • 40
0

In my case the destroyed object only returned true for the expression my_obj.Equals(null) when doing a null check. All the others (e.g. my_obj == null or my_obj is null) returned false.

Ling
  • 449
  • 6
  • 21