1. 程式人生 > 其它 >自定義報錯返回_Custom == operator Unity 自定義 == 操作符

自定義報錯返回_Custom == operator Unity 自定義 == 操作符

技術標籤:自定義報錯返回

一、個人結論

在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 研究了這個問題,得出了同樣的結論。快取程式碼如下:

private Transform m_CachedTransform
public Transform transform
{
  get
  {
    if (m_CachedTransform == null)
      m_CachedTransform = InternalGetTransform();
    return m_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 檢查比預期的更昂貴,這是快取沒有帶來任何速度優勢的原因。這導致了“如果我們錯過了它,有多少使用者會錯過它?”,這導致了這篇博文:)

參考連結:
原文地址
網上中文翻譯

by 2020-10-09 週五 晚上