看啥推荐读物
专栏名称: 炉石不传说
Unity程序猿一枚,爱玩游戏,更爱做自己喜...
今天看啥  ›  专栏  ›  炉石不传说

Custom == operator Unity 自定义 == 操作符

炉石不传说  · 简书  ·  · 2020-10-09 19:09

一、个人结论

在Unity中重写了“==”比较操作符,官方说是出于两个原因:

1、在编辑器模式下面重写“==”操作符,可以让报错NullReferenceException有更多的堆栈信息,方便开发者定位问题,而且在编辑器的Inspector面板上面还会高亮提示是哪个对象为null。

2、由于重写“==”操作符是在C++层处理的,C#层只是一个指向C++对象的一个指针。这样就会出现一个问题,当C++层的对象被Destroy掉了,C#层对象还存在,如果用“==”来比较,Unity会告诉你为true,但是C#层确不是为null,如果C#的对象被null了,但是C++层还存储,那么你比较“==”返回的值确实false。所有继承自Unity.object对象的类都会有这个问题。用 ?? 或 ?. 操作同样会有这样的问题。

3、一个有趣的测试,在缓存gameObject的时候用了GetComponent()函数,结果这个函数的开销比缓存的开销还要大。

二、原文翻译

When you do this in Unity:

if(myGameObject ==null)

{

}

Unity does something special with the == operator. Instead of what most people would expect, we have a special implementation of the == operator.

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

This serves two purposes:

这有两个目的:

1) When a MonoBehaviour has fields, in the editor only[1], we do not set those fields to “real null”, but to a “fake null” object. Our custom == operator is able to check if something is one of these fake null objects, and behaves accordingly. While this is an exotic setup, it allows us to store information in the fake null object that gives you more contextual information when you invoke a method on it, or when you ask the object for a property. Without this trick, you would only get a NullReferenceException, a stack trace, but you would have no idea which GameObject had the MonoBehaviour that had the field that was null. With this trick, we can highlight the GameObject in the inspector, and can also give you more direction: “looks like you are accessing a non initialised field in this MonoBehaviour over here, use the inspector to make the field point to something”.

当单行为有字段时,在编辑器中只有[1],我们不会将这些字段设置为“real null”,而是设置为“fake null”对象。我们的自定义 == 操作符能够检查某个对象是否是这些伪 null 对象之一,并相应地进行操作。虽然这是一种奇特的设置,但它允许我们将信息存储在伪 null 对象中,当您在该对象上调用方法或向该对象请求属性时,假 null 对象会提供更多上下文信息。如果没有这个技巧,你只会得到一个 NullReferenceException ,一个堆栈跟踪,但是你不知道哪个 GameObject 具有字段为 null 的 monobehavior 。

purpose two is a little bit more complicated.

目的二有点复杂。

2) When you get a c# object of type “GameObject”[2], it contains almost nothing. this is because Unity is a C/C++ engine. All the actual information about this GameObject (its name, the list of components it has, its HideFlags, etc) lives in the c++ side. The only thing that the c# object has is a pointer to the native object. We call these c# objects “wrapper objects”. The lifetime of these c++ objects like GameObject and everything else that derives from UnityEngine.Object is explicitly managed. These objects get destroyed when you load a new scene. Or when you call Object.Destroy(myObject); on them. Lifetime of c# objects gets managed the c# way, with a garbage collector. This means that it’s possible to have a c# wrapper object that still exists, that wraps a c++ object that has already been destroyed. If you compare this object to null, our custom == operator will return “true” in this case, even though the actual c# variable is in reality not really null.

当你得到一个类型为“ GameObject ”[2]的c#对象时,它几乎不包含任何内容。这是因为Unity是一个C/ c++ 引擎。关于 GameObject (游戏物体)的所有实际信息(它的名字,它的组件列表,它的HideFlags 等等)都存在于 c++ 端。c# 对象只有一个指向本机对象的指针。我们将这些 c# 对象称为“包装器对象”。这些 c++ 对象的生命周期,如 GameObject 和 UnityEngine 派生的所有其他对象。对象是显式管理的。当你加载一个新场景时,这些对象会被销毁。或者当你调用 object.destroy (myObject); 在他们身上。c# 对象的生命周期通过垃圾收集器以 c# 方式管理。这意味着可以有一个仍然存在的 c# 包装器对象,它包装一个已经销毁的 c++ 对象。如果将这个对象与 null 进行比较,我们的自定义 == 运算符在这种情况下将返回“ true ”,尽管实际的 c# 变量实际上并不是 null 。

While these two use cases are pretty reasonable, the custom null check also comes with a bunch of downsides.

虽然这两个用例非常合理,但是自定义空检查也有很多缺点。

It is counterintuitive.

这是违反直觉的

Comparing two UnityEngine.Objects to eachother or to null is slower than you’d expect.

比较两个 UnityEngine.Objects 之间或对象之间为空的速度比您预期的要慢。

The custom ==operator is not thread safe, so you cannot compare objects off the main thread. (this one we could fix).

自定义 == 操作符不是线程安全的,因此不能在主线程之外比较对象。(这个我们可以修正)

It behaves inconsistently with the ?? operator, which also does a null check, but that one does a pure c# null check, and cannot be bypassed to call our custom null check.

它的行为与 ?? 不一致。?? 也执行空检查,但该操作符执行纯 c# 空检查,不能通过旁路调用我们的自定义空检查。

Going over all these upsides and downsides, if we were building our API from scratch, we would have chosen not to do a custom null check, but instead have a myObject.destroyed property you can use to check if the object is dead or not, and just live with the fact that we can no longer give better error messages in case you do invoke a function on a field that is null.

复习所有这些优点和缺点,如果我们从头开始构建我们的 API 时,我们会选择不去做一个定制的 null 检查,而是有一个 myObject.destroyed 属性可以用来检查对象已死,就住在一起的事实,我们可以不再提供更好的错误消息,以防你调用一个函数在一个字段,该字段为空。

What we’re considering is wether or not we should change this. Which is a step in our never ending quest to find the right balance between “fix and cleanup old things” and “do not break old projects”. In this case we’re wondering what you think. For Unity5 we have been working on the ability for Unity to automatically update your scripts (more on this in a subsequent blogpost). Unfortunately, we would be unable to automatically upgrade your scripts for this case. (because we cannot distinguish between “this is an old script that actually wants the old behaviour”, and “this is a new script that actually wants the new behaviour”).

我们正在考虑的是我们是否应该改变这一现状。这是我们在“修复和清理旧事物”和“不要破坏旧项目”之间找到正确平衡的永无止境的探索中的一步。在这种情况下,我们想知道你的想法。对于Unity5,我们一直致力于Unity自动更新脚本的功能(更多信息请见后续的博文)。不幸的是,在这种情况下,我们无法自动升级您的脚本。(因为我们无法区分“这是一个实际上想要旧行为的旧脚本”和“这是一个实际上想要新行为的新脚本”)。

We’re leaning towards “remove the custom == operator”, but are hesitant, because it would change the meaning of all the null checks your projects currently do. And for cases where the object is not “really null” but a destroyed object, a nullcheck used to return true, and will if we change this it will return false. If you wanted to check if your variable was pointing to a destroyed object, you’d need to change the code to check “if (myObject.destroyed) {}” instead. We’re a bit nervous about that, as if you haven’t read this blogpost, and most likely if you have, it’s very easy to not realise this changed behaviour, especially since most people do not realise that this custom null check exists at all.[3]

我们倾向于“删除custom == operator”,但是我们犹豫不决,因为它会改变项目当前执行的所有null检查的含义。如果对象不是” really null “而是一个销毁的对象,一个用于返回 true 的 nullcheck ,如果我们改变这个它会返回 false 。如果您想检查您的变量是否指向一个销毁的对象,您需要更改代码来检查“ If (myObject.destroyed){} ”。我们对此感到有点紧张,就好像您没有读过这篇博客文章一样,如果您读过,很可能您没有意识到这种改变的行为,特别是因为大多数人根本没有意识到这种自定义空检查的存在。

If we change it, we should do it for Unity5 though, as the threshold for how much upgrade pain we’re willing to have users deal with is even lower for non major releases.

如果我们改变它,我们应该在 Unity5 上做,因为对于非主流版本,我们愿意让用户承受的升级痛苦的阈值更低。

What would you prefer us to do? give you a cleaner experience, at the expense of you having to change null checks in your project, or keep things the way they are?

你希望我们做什么?为您提供更清晰的体验,您必须在项目中更改null检查,或者保持原样?

Bye, Lucas (@lucasmeijer)

[1] We do this in the editor only. This is why when you call GetComponent() to query for a component that doesn’t exist, that you see a C# memory allocation happening, because we are generating this custom warning string inside the newly allocated fake null object. This memory allocation does not happen in built games. This is a very good example why if you are profiling your game, you should always profile the actual standalone player or mobile player, and not profile the editor, since we do a lot of extra security / safety / usage checks in the editor to make your life easier, at the expense of some performance. When profiling for performance and memory allocations, never profile the editor, always profile the built game.

我们只在编辑器中这样做。这就是为什么当您调用 GetComponent() 来查询不存在的组件时,您会看到 c# 内存分配正在发生,因为我们正在新分配的伪 null 对象中生成这个定制的警告字符串。这种内存分配不会在构建的游戏中发生。这是一个很好的例子,为什么如果你分析你的游戏,你应该总是剖面实际的独立播放器或移动的球员,而不是概要文件编辑器,因为我们做了很多额外的安全/安全/使用检查在编辑器中使你的生活更容易,以牺牲一些性能。在分析性能和内存分配时,永远不要分析编辑器,而应该分析构建的游戏。

[2] This is true not only for GameObject, but everything that derives from UnityEngine.Object

这不仅适用于 GameObject ,也适用于 UnityEngine.Object 中的所有内容

[3] Fun story: I ran into this while optimising GetComponent() performance, and while implementing some caching for the transform component I wasn’t seeing any performance benefits. Then @jonasechterhoff looked at the problem, and came to the same conclusion. The caching code looks like this:

有趣的故事:我在优化 GetComponent() 性能时遇到了这种情况,在为转换组件实现一些缓存时,我没有看到任何性能优势。然后 @jonasechterhoff 研究了这个问题,得出了同样的结论。缓存代码如下:

privateTransform m_CachedTransform

publicTransform transform

{

get

{

if(m_CachedTransform ==null)

m_CachedTransform = InternalGetTransform();

returnm_CachedTransform;

}

}

Turns out two of our own engineers missed that the null check was more expensive than expected, and was the cause of not seeing any speed benefit from the caching. This led to the “well if even we missed it, how many of our users will miss it?”, which results in this blogpost :)

原来我们的两个工程师没有注意到 null 检查比预期的更昂贵,这是缓存没有带来任何速度优势的原因。这导致了“如果我们错过了它,有多少用户会错过它?”,这导致了这篇博文:)

参考链接如下:

原文地址

网上翻译地址




原文地址:访问原文地址
快照地址: 访问文章快照