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