您想找什么?
Engine & platform

自定义 == 运算符,是否应保留?

LUCAS MEIJER / UNITY TECHNOLOGIESContributor
May 16, 2014|5 Min
自定义 == 运算符,是否应保留?
为方便起见,此网页已进行机器翻译。我们无法保证翻译内容的准确性或可靠性。如果您对翻译内容的准确性有疑问,请参阅此网页的官方英文版本。

在 Unity 中执行此操作时:

if (myGameObject == null) {}

Unity 对 == 运算符做了一些特殊处理。与大多数人所期望的不同,我们有一个特殊的 == 运算符实现。

这样做有两个目的:

1) 当 MonoBehaviour 具有字段时,仅在编辑器中[1],我们不会将这些字段设置为 "真正的 null",而是设置为一个 "假 null "对象。我们自定义的 == 运算符能够检查某物是否是这些假 null 对象之一,并做出相应的处理。虽然这是一个奇特的设置,但它允许我们在假 null 对象中存储信息,当你调用它的方法或向对象询问属性时,可以获得更多上下文信息。如果没有这个技巧,你只会得到一个 NullReferenceException(空引用异常)和堆栈跟踪,但你根本不知道哪个 GameObjects 的 MonoBehaviour 中的字段是 null。有了这个小技巧,我们就可以在检查器中突出显示 GameObject,还可以给你提供更多的指导:"看起来你正在访问这个 MonoBehaviour 中一个未初始化的字段,使用检查器让这个字段指向某个东西"。

目的二则要复杂一些。

2) 当您得到一个类型为 "GameObject"[2] 的 c# 对象时,它几乎不包含任何内容。这是因为 Unity 是一个 C/C++ 引擎。该 GameObjects 的所有实际信息(名称、组件列表、HideFlags 等)都保存在 c++ 端。c# 对象唯一拥有的就是一个指向本地对象的指针。我们称这些 C# 对象为 "封装对象"。 这些 c++ 对象(如 GameObject 和其他从 UnityEngine.Object 派生的所有对象)的生命周期是显式管理的。加载新场景时,这些对象会被销毁。或者在它们身上调用 Object.Destroy(myObject); 时。C# Objective-C 的生命周期是通过垃圾回收器以 C# 方式管理的。这意味着有可能出现一个仍然存在的 c# 封装对象,封装了一个已经被销毁的 c++ 对象。如果将该对象与 null 进行比较,在这种情况下,我们自定义的 == 运算符将返回 "true",尽管实际的 c# 变量实际上并不是真正的 null。

虽然这两种用例都很合理,但自定义 null 检查也有很多缺点。

- 这是反直觉的。

- 将两个 UnityEngine.Objects 互相比较或与 null 比较的速度比想象的要慢。

- 自定义 == 操作符不是线程安全的,因此无法在主线程外比较对象。(这个问题我们可以解决)。

- 它与同样进行 null 检查的"? "操作符的行为不一致,但"? "操作符进行的是纯粹的 c# null 检查,不能绕过它来调用我们自定义的 null 检查。

综上所述,如果我们从头开始构建 API,我们就不会选择自定义 null 检查,而会选择使用 myObject.destroyed 属性来检查对象是否已死亡,同时也不会再在调用空字段函数时给出更好的错误信息。

我们正在考虑的是,是否应该改变这种状况。这也是我们在 "修复和清理旧事物 "与 "不破坏旧项目 "之间寻找正确平衡的永无止境的探索中迈出的一步。在这种情况下,我们想知道您的想法。对于 Unity5,我们一直在开发 Unity 自动更新脚本的功能(后续博文将对此进行详细介绍)。遗憾的是,在这种情况下,我们无法自动升级您的脚本。(因为我们无法区分 "这是一个旧脚本,实际上想要使用旧行为 "和 "这是一个新脚本,实际上想要使用新行为")。

我们倾向于 "移除自定义 == 操作符",但又有些犹豫,因为这将改变您的项目目前所做的所有 null 检查的含义。如果对象不是 "真正的 null",而是一个已销毁的对象,那么 nullcheck 会返回 true,如果我们改变它,它就会返回 false。如果要检查变量是否指向已销毁的对象,则需要将代码改为检查 "if (myObject.destroyed) {}"。我们对此有点紧张,因为如果你没有读过这篇博文,而且很有可能如果你读过,就很容易意识不到这种改变了的行为,尤其是大多数人根本没有意识到这种自定义 null 检查的存在[3]。

如果要改,也应该在 Unity5 上改,因为在非主要版本中,我们愿意让用户承受升级痛苦的门槛更低。

您希望我们怎么做?是给您一个更简洁的体验,而您却不得不修改项目中的 null 校验,还是保持原样?

再见,卢卡斯(@lucasmeijer)

[1] 我们只在编辑器中这样做。这就是为什么当你调用 GetComponent() 来查询一个不存在的组件时,你会看到 C# 内存分配,因为我们在新分配的假 null 对象中生成了这个自定义警告字符串。这种内存分配不会在内置游戏中发生。这是一个很好的例子,说明了为什么如果要对游戏进行剖析,应该始终对实际的单机版玩家或手机版玩家进行剖析,而不是对编辑器进行剖析,因为我们在编辑器中进行了大量额外的安全/保障/使用检查,以让您的生活更轻松,但也牺牲了一些性能。在对性能和内存分配进行剖析时,永远不要对编辑器进行剖析,而要始终对构建的游戏进行剖析。

[2] 这不仅适用于 GameObjects,也适用于从 UnityEngine.Object 派生的所有对象。

[3] 有趣的故事我在优化 GetComponent<T>() 性能时遇到了这个问题,在为转换组件实施缓存时,我没有看到任何性能优势。随后,@Jonasechterhoff研究了这个问题,并得出了相同的结论。缓存代码如下所示

private Transform m_CachedTransform
public Transform transform
{
  get
  {
    if (m_CachedTransform == null)
      m_CachedTransform = InternalGetTransform();
    return m_CachedTransform;
  }
}

结果发现,我们自己的两名工程师忽略了 null 检查的成本比预期的要高,这也是缓存没有带来任何速度优势的原因。这引发了 "如果连我们都错过了,我们的用户中会有多少人错过呢?)