[譯]Unity – Custom == operator, should we keep it?

很有趣的…喔不!同時也很重要的事情,但我相信9成以上的 unity 使用者應該都不知道這事情吧!!

簡單來說到目前(4.5.2f2)為止,unity上衍生自 UnityEngine.Object 的物件的 null check 都不是原生的~

原文底下的回應是我目前看過最熱烈的(?),而且目前回應被鎖定中的樣子@@

當你在 Unity 做這個:

if (myGameObject == null) {}

Unity 會對這個 == 運算子(operator)做些特殊的事情。我們在 == 上做了不同於大眾預期的特別實作。

這有兩個目的:

1) 只有在 editor[註1],當 MonoBehaviour 有個欄位(fields),我們並不會真的將之設為一個"真正的 null",而是一個"假的 null"物件。這個特製的 == 可以用來確認目標是否為假物件,並做對應的處裡。雖然這是一個很詭異的設定,但可以讓我們儲存一些資訊在假物件中。當你調用某個 method 或是存取某個屬性時,它可以提供更多訊息。假如沒有這個小把戲,你就只能得到一個 NullReferenceException & stack trace,但你就無法得知這個 null 是在哪個 GameObject 上的哪個 MonoBehaviour 的哪個欄位了。有了這個手段,我們可以在 inspector 中 highlight 該物件,並給你更多方向:"看起來似乎是你正在存取在這個 MonoBehaviour 上,一個尚未初始化的欄位。透過 inspector 指定該欄位的物件。"

目的二就複雜了些。

2) 當你拿到一個類型是 “GameObject"[註2] 的 C# 物件時,它甚麼都沒有。這是因為 Unity 是一個 C/C++ 的引擎。GameObject 裡面所有的真實資訊(它的名稱、擁有的組件列表、HideFlags等等)都存在於 C++ 端。C# 物件唯一擁有的只有一個指向原生物件的指標。我們稱那些 C# 物件為"wrapper objects"。那些 GameObject 或是其他衍伸自 UnityEngine.Object 的C++ 物件們的壽命,都有明確地被管理著。這些物件會在你讀取一個新場景時被摧毀,或是你呼叫 Object.Destroy(myObject); 的時候。至於 C# 物件的生存時間則是由 C# 的方式管理,也就是 GC(garbage collector)。這表示,即使 C# 物件仍然存在,但有可能它包裹的 C++ 物件早已經被摧毀了。如果你在這個時間點比較這個物件是否為 null 的話,即使該 C# 物件實際上並不是真正的 null ,但我們的特製 == 會回給你 “true"。

雖然以上兩點理由相當合理,但這個特製的 null check 機制也是帶著一些缺點。

● 有悖常理。
● 兩個 UnityEngine.Objects 互相比較或是與 null 的比較,都比你想像的來得遲緩。
● 這個特製的 == 並不是執行緒安全(Thread Safe)的,所以你不能在主要執行緒之外比較物件。(這個是我們可以修正的)
● 它的行為和 ?? 這個運算子是不一致的,它也是一個 null check 沒錯,但它不是純粹的 C# null check,也就不能繞道使用我們特製的 null check 了。

看了這些優缺點,如果我們重頭打造 API,我們不會再選擇使用特製的 null check,取而代之的是一個 myObject.destroyed 的屬性讓你可以確認該物件是死是活。不過也同時存在著一個事實,當你 invoke 一個在欄位上的 function 是 null 的時候,我們無法再提供更好錯誤訊息。

我們正在考慮是否要改變這個。這是我們不斷地在"修改與清除過時的東西"與"不破壞既有的專案"之間,尋求最好的平衡點中的一步。關於這點,我們很好奇你們的想法。在 Unity5 我們會讓 Unity 可以自動升級你的 script (在之後的文章會再詳述)。不幸地,我們無法在這部份幫你們自動升級。(因為我們無法分辨出"舊的 script 是否是我們要的舊行為"以及"新的 script 是否是我們要的新行為")。

我們想要朝"移除特製的 null 運算子"前進,但一直都很躊躇。
因為這意味著它將會改動你的專案中所有使用 null check 的地方。
像是有些地方並非"真正的 null",但它是一個被摧毀的物件,在此之前的 null check 都是回傳 true,但我們之後會將它改為回傳 false。
如果你是想要檢查你的變數指向的物件是否已經被摧毀的話,你需要改成檢查"if (myObject.destroyed) {}"。
對於這點我們有些緊張,如果你沒有看過,或者即使你看過了這篇文章,也很不是很容易去意識到這個被改變的行為,尤其當大部分的人根本沒有意識到這點時…[註3]

如果我們要改變它,應該會在 Unity5 實行。但因為有需要使用者經歷陣痛處理升級的這個門檻,得要在之後的非主要版本中釋出。

你喜歡我們怎們做?給你一個更乾淨俐落的體驗,但需要你去更改專案中的 null check。或是,保持現狀?

Bye, Lucas (@lucasmeijer)

[1] 我們只在 editor 做這個。這是為何當你呼叫 GetComponent() 搜尋該物件卻不存在時,你會看到一個 C# 的記憶體配置。這是因為我們在新配置的假物件的內部,產生了這個特製的警告訊息的關係。這個記憶體配置並不會發生在一般的遊戲內。這是為何你應該要在真正的 standalone/mobile player 而非在 editor 上使用 profile 的一個非常好的範例。當我們執行大量額外的 security / safety / usage 檢查來讓你的 editor 生活更加美好的代價就是一些效能的損耗。如果要做效能和記憶體配置的 profile 時,請務必在建置的遊戲而非 editor 中使用。

[2] 這不僅限於 GameObject,而是所有衍生自 UnityEngine.Object 的物件。

[3] 有趣的小故事:我跑過了這個透過緩存(caching) transform 組件來優化 GetComponent<T>() 效能的 code,但我沒有看到任何的效益。然後 @jonasechterhoff 看出了問題,給了相同的結論。這個緩存的 code 像是這樣:

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

我們有兩位工程師忽略了這個 null check 會比期望的還要昂貴的事情,這是為何無法從緩存看到任何速度效益的原因。
這衍伸出了"如果連我們都疏忽了,會有多少我們的使用者疏忽了呢?" 這就是這篇文章出現的理由:)

原文『Custom == operator, should we keep it?

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 變更 )

Twitter picture

You are commenting using your Twitter account. Log Out / 變更 )

Facebook照片

You are commenting using your Facebook account. Log Out / 變更 )

Google+ photo

You are commenting using your Google+ account. Log Out / 變更 )

連結到 %s