Engine & platform

カスタム==演算子、残すべきか?

LUCAS MEIJER / UNITY TECHNOLOGIESContributor
May 16, 2014|5 分
カスタム==演算子、残すべきか?
このページは機械翻訳されています。正確性のため、また情報源として原語バージョンを表示するには

Unityでこれを行う場合:

if (myGameObject == null) {}

Unityは==演算子で特別なことをする。多くの人が期待する代わりに、私たちは==演算子の特別な実装を持っている。

これには2つの目的がある:

1) MonoBehaviourにフィールドがある場合、エディタのみ[1]では、それらのフィールドを「本物のnull」ではなく、「偽のnull」オブジェクトに設定します。私たちのカスタム==演算子は、何かがこれらの偽NULLオブジェクトのいずれかであるかどうかをチェックし、それに応じて動作することができます。これはエキゾチックなセットアップだが、偽のヌル・オブジェクトに情報を保存することで、そのオブジェクトに対してメソッドを呼び出したり、オブジェクトにプロパティを求めたりする際に、より多くのコンテキスト情報を与えることができる。このトリックがなければ、NullReferenceExceptionが発生し、スタック・トレースが表示されるだけで、どのGameObjectがNullのフィールドを持つMonoBehaviourを持っているかはわからない。このトリックを使えば、インスペクタでGameObjectをハイライトすることができ、「このMonoBehaviourの初期化されていないフィールドにアクセスしているようだから、インスペクタを使ってフィールドが何かを指すようにしなさい」という指示を出すこともできる。

目的2はもう少し複雑だ。

2) "GameObject"[2]型のc#オブジェクトを取得すると、ほとんど何も含まれていません。これはUnityがC/C++エンジンであるためです。このGameObjectに関する実際の情報(名前、持っているコンポーネントのリスト、HideFlagsなど)はすべてc++側にある。c#オブジェクトが持っているのは、ネイティブ・オブジェクトへのポインタだけである。これらのC#オブジェクトを「ラッパー・オブジェクト」と呼ぶ。 UnityEngine.Objectから派生するGameObjectやその他すべてのオブジェクトのライフタイムは、明示的に管理されます。これらのオブジェクトは、新しいシーンをロードすると破棄されます。あるいは、それに対してObject.Destroy(myObject);を呼び出したとき。c#のオブジェクトの寿命は、ガベージコレクタを使ってc#流に管理される。つまり、すでに破棄されたc++オブジェクトを、まだ存在するc#ラッパー・オブジェクトでラップすることが可能なのだ。このオブジェクトをnullと比較すると、実際のc#変数が実際にはnullでなくても、カスタム==演算子はこの場合「true」を返す。

これら2つの使用例はかなり合理的だが、カスタムヌルチェックには多くの欠点もある。

- 直感に反する。

- 2つのUnityEngine.Objectを互いに比較したり、nullと比較したりするのは、予想以上に時間がかかります。

- カスタム==演算子はスレッドセーフではないので、メインスレッドから離れたオブジェクトを比較することはできません。(これは修正できる)。

- 演算子もNULLチェックを行うが、そちらは純粋なC#のNULLチェックを行うため、カスタムNULLチェックを呼び出すためにバイパスすることはできない。

これらの長所と短所を考慮すると、もしAPIをゼロから構築するのであれば、カスタムNULLチェックは行わず、代わりにmyObject.destroyedプロパティを使ってオブジェクトが死んでいるかどうかをチェックすることにしただろう。

私たちが考えているのは、これを変えるべきかどうかということだ。これは、"古いものを直してきれいにする "ことと "古いプロジェクトを壊さない "ことの適切なバランスを見つけるという、私たちの終わりのない探求の一歩である。この場合、我々はあなたがどう考えるか気になる。Unity5では、Unityがスクリプトを自動的にアップデートする機能に取り組んでいます(これについては、次のブログ記事で詳しく説明します)。残念ながら、この場合、スクリプトを自動的にアップグレードすることはできません。(「これは古いスクリプトで、実際に古い動作を望んでいる」のか、「これは新しいスクリプトで、実際に新しい動作を望んでいる」のかを区別できないからです)。

私たちは「カスタム==演算子を削除する」方向に傾いていますが、現在プロジェクトが行っているすべてのNULLチェックの意味が変わってしまうため、躊躇しています。また、オブジェクトが「本当にnull」ではなく、破壊されたオブジェクトである場合、nullチェックはこれまでtrueを返していたが、これを変更するとfalseを返すようになる。変数が破壊されたオブジェクトを指しているかどうかをチェックしたい場合は、代わりに「if (myObject.destroyed) {}」をチェックするようにコードを変更する必要がある。というのも、もしあなたがこのブログ記事を読んでいないなら、そしておそらく読んでいるのなら、この変更された動作に気づかないのはとても簡単だからです。

もし変更するのであれば、Unity5で行うべきでしょう。メジャーリリースでない場合、ユーザーにどれだけのアップグレードの痛みを負わせることができるかの閾値はさらに低くなりますから。

プロジェクトのヌルチェックを変更しなければならない犠牲を払ってでも、よりクリーンなエクスペリエンスを提供するのか、それとも現状を維持するのか。

さようなら、ルーカス(@lucasmeijer)

[1]エディターでのみ行います。GetComponent()を呼び出して存在しないコンポーネントを検索すると、C#のメモリ割り当てが発生するのはこのためです。このメモリ割り当ては、ビルドされたゲームでは起こらない。なぜなら、エディターでは、あなたの生活を楽にするために、セキュリティ/安全性/使用方法のチェックをたくさん行っているからです。パフォーマンスとメモリ割り当てのためにプロファイリングを行う場合、エディタのプロファイリングは決して行わず、常にビルドされたゲームのプロファイリングを行います。

[2]これはGameObjectだけでなく、UnityEngine.Objectから派生するものすべてに当てはまります。

[3] 楽しい話だ:GetComponent<T>()のパフォーマンスを最適化しているときに、この問題に遭遇しました。transformコンポーネントのキャッシュを実装していたのですが、パフォーマンス上の利点は見られませんでした。その後、@jonasechterhoffがこの問題を検討し、同じ結論に達した。キャッシングコードは次のようになる:

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

私たちのエンジニアの2人が、ヌルチェックが予想以上に高価であることを見落としており、キャッシュによるスピードの恩恵が得られない原因であることが判明した。その結果、「私たちでさえ見逃したのだから、いったい何人のユーザーが見逃すのだろう?)