-
var a = new GameObject("A");
-
var b = new GameObject("B");
-
Destroy(a);
-
// Wait as mutch as you like before
-
Debug.Log((a ?? b).name); // It will give a MissingReferenceException
public GameObject c; c?.gameObject.SetActive(false); // It will give a MissingReferenceException原因
Unity对 == 运算符做了一些自以为是的反人类特殊处理。
太长不看版目的:尽量不要报空,即便是真空了,我也包一层 ”假空“ 指向这个真空,而不是直接报真空。
详细版:
目的一:
当一个MonoBehaviour有字段时,仅在编辑器中[1],我们不会将这些字段设置为 "真正的null",而是设置为 "假null "对象。我们的自定义==操作符能够检查某些东西是否是这些假空对象之一,并采取相应的行为。虽然这是一个奇特的设置,但它允许我们在假空对象中存储信息,当你对它调用一个方法时,或者当你向该对象询问一个属性时,可以给你更多的上下文信息。如果没有这个技巧,你只会得到一个NullReferenceException,一个堆栈跟踪,但你不知道哪个GameObject的MonoBehaviour的字段是空的。有了这个技巧,我们可以在检查器中突出显示GameObject,也可以给你更多的指导。"看起来你正在访问这个MonoBehaviour中的一个未初始化的字段,使用检查器使这个字段指向某个东西"。
目的二有点复杂。
当你得到一个 "GameObject"[2]类型的c#对象时,它几乎什么都不包含。这是因为Unity是一个C/C++引擎。所有关于这个GameObject的实际信息(它的名字、它的组件列表、它的HideFlags等等)都在C++端。c#对象拥有的唯一东西是一个指向本地对象的指针。我们称这些c#对象为 "包装器对象"。 这些C++对象的生命周期,如GameObject和其他所有派生自UnityEngine.Object的对象,都是显式管理的。当你加载一个新的场景时,这些对象会被销毁。或者当你对它们调用Object.Destroy(myObject);时。c#对象的生命周期是用c#方式管理的,有一个垃圾收集器。这意味着有可能有一个仍然存在的c#包装对象,它包装了一个已经被销毁的c++对象。如果你将这个对象与null进行比较,在这种情况下,我们的自定义==操作符将返回 "true",尽管实际的c#变量实际上并不是真的null。
缺点虽然这两个用例相当合理,但自定义的null检查也带来了一堆缺点。
1. 它是反直觉的。 2. 将两个UnityEngine.Objects相互比较或与null比较的速度比你想象的要慢。 3. 自定义的==运算符不是线程安全的,所以你不能在主线程之外比较对象。(这个问题我们可以解决)。 4. 它的行为与??操作符不一致,后者也做一个空值检查,但是那个操作符做的是纯c#的空值检查,不能被绕过来调用我们的自定义空值检查。
考虑到所有这些优点和缺点,如果我们从头开始构建我们的API,我们会选择不做一个自定义的空值检查,而是有一个myObject.destroy属性,你可以用它来检查对象是否已经死亡,而只是忍受这样一个事实:如果你真的在一个空字段上调用一个函数,我们不能再给出更好的错误信息。
我们正在考虑的是,我们是否应该改变这一点。这是我们在 "修复和清理旧事物 "和 "不破坏旧项目 "之间找到正确平衡的永无止境的探索的一个步骤。在这种情况下,我们想知道你的想法。对于Unity5,我们一直在努力使Unity能够自动更新你的脚本(关于这一点,在随后的博文中会有更多介绍)。不幸的是,对于这种情况,我们将无法自动升级你的脚本。(因为我们无法区分 "这是一个实际想要旧行为的旧脚本",和 "这是一个实际想要新行为的新脚本")。
我们倾向于 "移除自定义的==运算符",但又犹豫不决,因为这将改变你的项目目前所做的所有空值检查的意义。对于对象不是 "真正的null "而是一个被破坏的对象的情况,null检查过去是返回真,如果我们改变这个,它将返回假。如果你想检查你的变量是否指向一个被破坏的对象,你需要将代码改为检查 "if (myObject.destroy) {}"。我们对此有点紧张,因为如果你没有读过这篇博文,而且很可能如果你读过了,就很容易意识不到这种改变后的行为,尤其是大多数人根本没有意识到这种自定义的空值检查的存在[3] 。
索引[1] 我们只在编辑器中这样做。这就是为什么当你调用GetComponent()查询一个不存在的组件时,你会看到C#的内存分配发生,因为我们在新分配的假空对象中生成了这个自定义的警告字符串。这种内存分配不会发生在内置游戏中。这是一个很好的例子,如果你要对你的游戏进行剖析,你应该总是对实际的独立玩家或移动玩家进行剖析,而不是对编辑器进行剖析,因为我们在编辑器中做了很多额外的安全性/安全/使用检查,以使你的生活更容易,但却牺牲了一些性能。当对性能和内存分配进行剖析时,永远不要对编辑器进行剖析,总是对构建的游戏进行剖析。[2] 这不仅适用于GameObject,而且适用于所有从UnityEngine.Object派生出来的东西。[3] 有趣的故事。我在优化GetComponent()性能时遇到了这个问题,在为变换组件实现一些缓存时,我没有看到任何性能上的好处。然后@jonasechterhoff看了看这个问题,也得出了同样的结论。缓存代码看起来像这样。
private Transform m_CachedTransform
public Transform transform
{
get
{
if (m_CachedTransform == null)
m_CachedTransform = InternalGetTransform();
return m_CachedTransform;
}
}
结果我们的两位工程师发现,空值检查的成本比预期的要高,这也是没有从缓存中看到任何速度优势的原因。这导致了 "如果连我们都忽略了它,那么我们有多少用户会忽略它呢?",这导致了这篇博文:)
C# (??) null-coalescing operator does not work for UnityEngine.Object types - Unity Forum
Custom == operator, should we keep it? | Unity Blog