为什么 C# 空条件运算符不适用于 Unity 可序列化变量?



我注意到,如果我有一些变量暴露给 Unity 检查器,例如:

[SerializeField] GameObject _tickIcon;

如果我将它们保留为未分配并尝试使用 null 条件运算符并在该对象上调用方法,则会收到一个异常,指出未分配变量。所以基本上不是这样做:

_tickIcon?.SetActive(false);

它迫使我这样做:

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

所以我猜这一定是特定于 Unity 运行时的东西,它并不是真正的空,但我可以检查空并且它可以工作。我真的不明白这一点。

它通常不适用于从UnityEngine.Object继承的任何内容!

tl;dr:???.运算符在System.Object(acaobject(级别工作,而 Unity 的==运算符在UnityEngine.Object级别工作。


由于他们在内部以不同的方式实现==/!=方法,因此被绕过了。请参阅自定义 == 运算符,我们应该保留它吗?

ReSharper在可能无意中绕过底层Unity引擎对象的生命周期检查中很好地解释了它

如果派生自UnityEngine.Object的类型使用空合并 (??( 或空传播或条件 (?.( 运算符,则会显示此警告。这些运算符不使用在UnityEngine.Object上声明的自定义相等运算符,因此会绕过检查以查看底层本机 Unity 引擎对象是否已销毁。为了澄清意图,最好使用显式null或布尔比较,或调用System.Object.ReferenceEquals()

UnityEngine.Object在某些情况下并不是真正null,但仍保留一些元数据。所以底层object(=System.Object(不是nullUnityEngine.Object的覆盖==运算符只是返回== nulltrue

原因是:c#UnityEngine.Object只是实际底层c++引擎代码之上的开发人员 API 层。自定义==implicit bool operator基本上都归结为

(源代码(

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

后台调用本机引擎代码,在这些代码中处理这些对象的实际实例并控制其生命周期。因此,例如,在Destroy对象后,在本机c++引擎中,该对象已被标记为已销毁,并且自定义==bool operator已经返回falsec#UnityEngine.Object仍然存在,直到它被实际垃圾回收(通常在帧的末尾,但也没有真正的保证(。

因此,像_tickIcon?.gameObjct抛出NullReferenceException这样的事情的主要原因是?.运算符只直接在底层object(System.Object(上工作,而UnityEngine.Object则在不同的级别上处理他们的自定义实现。

例如之后

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

您会注意到,您不会得到一个正常的NullReferenceException如果它实际上是null的情况,而是得到一个 Unity 海关MissingReferenceException告诉您为什么抛出异常的可能原因。


长话短说:作为解决方案UnityEngine.Object具有隐式bool运算符

对象是否存在?

您应该始终检查从UnityEngine.Object派生的任何内容是否存在,如下所示:

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

或显式(因为这将使用自定义==/!=运算符(

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

只是为了重新混合这个问题的语法,当尝试这样的事情时:

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

相反,请尝试以下操作:

//OK
Vector3 targetPos = target ? target.position : Vector3.zero;

在我的情况下,销毁的对象仅在执行空检查时返回表达式my_obj.Equals(null)true。所有其他(例如my_obj == nullmy_obj is null(返回false

最新更新