Why does C# null-conditional operator not work with Unity serializable variables?
Asked Answered
H

4

26

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.

Hyperemia answered 1/7, 2020 at 13:41 Comment(0)
K
35

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.


Unity has a custom implementation of == for UnityEngine.Object which actually is a hook down into the underlying c++ engine. 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());
    }

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

background calls into the native engine code where the actual instances of those objects 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);
}
Kraft answered 1/7, 2020 at 13:45 Comment(10)
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!Hyperemia
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.Shiism
@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 itKraft
Thanks for the info! Do you know of a situation in which ? works with UnityEngine.Object?Shiism
@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)Kraft
Can you implement a custom ?? and ?. in your own classes that derive from UnityEngine.Object to make this work?Sufficiency
@KyleDelaney no. These belong to the Non-overloadable operators (bottom row in the table)Kraft
Then I wonder how this package works: assetstore.unity.com/packages/tools/utilities/…Sufficiency
@KyleDelaney you'd have to ask the author of that package. I can imagine some interceptor thing maybe .. this was done for Trasnform properties The tool will use IL Weaving and will redirect all the set calls to transform.position, transform.rotation and transform.scale to TransformSetterCallInterceptor where you could add any actions needed. ... not sure if this can also be done for overriding Unity's null check e.g.Kraft
@KyleDelaney didn't dive very deep but at first glance they indeed do some deep injections modifying the assembly .. what exactly is quite well obfuscated and hard to decipher .. would have to performance check to see if and how much impact it has there .. but besides that it actually looks quite neat and isn't that expensiveKraft
S
1

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;
Stirrup answered 3/6, 2022 at 8:39 Comment(0)
W
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.

Wines answered 30/11, 2022 at 15:59 Comment(0)
I
0

You can use this asset from the asset store to use the null conditional and null propagation operators safely on GameObjects: https://assetstore.unity.com/packages/tools/utilities/smooth-operator-271378

Inchoative answered 12/2 at 4:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.