Unityの偽装nullの話
こんにちは、Nobollelエンジニアの古屋です。主にUnityでiOS/Androidのクライアントサイドの開発をしています。
今回は、UnityでオブジェクトをDestroyしたときの挙動に関する小ネタを書こうと思います。
Destroyされたオブジェクトは本当はnullではない?
UnityでオブジェクトをDestroyするとnullになります。
void Start() { var obj = new UnityEngine.Object(); Destroy(obj); if (obj == null) { Debug.Log("obj is destroyed"); } }
Unityを使ってるとこれは普通なのですが、C#的に考えると、オブジェクトが勝手にnullになるのはおかしいです。実際のところ、Destroyされたオブジェクトは「nullっぽく振る舞うオブジェクト」であって、参照自体は生きています。
参照が生きていることを確認するために、System.WeakReferenceを使ってオブジェクトを追跡してみると、Destroyしても永久に「alive=True」であることがわかります。
UnityEngine.Object obj; WeakReference weakRef; void Awake() { obj = new UnityEngine.Object(); weakRef = new WeakReference(obj); Destroy(obj); } void Update() { Debug.Log("alive=" + weakRef.IsAlive); }
objが死なないのは、objの参照が「偽装null」として残っているせいでGCが回収できないからです。試しにobjをメンバ変数ではなくローカル変数として定義すると、Awakeを抜けた時点で参照がなくなり、次のGCのタイミングで「alive=False」になります。
また、System.Object.ReferenceEqualsを使っても、objがnullでないことが確認できます。
void Start() { var obj = new UnityEngine.Object(); Destroy(obj); if (!object.ReferenceEquals(obj, null)) { Debug.Log("obj is not null"); } }
何が起きているのか
- CUSTOM == OPERATOR, SHOULD WE KEEP IT?blogs.unity3d.com
この記事にもあるように、Unityのオブジェクトは==演算子をオーバーロードしてnullを偽装しているようです。たぶん、中にDestroyされたかどうかのフラグを持っていて、「本物のnull」でなければそのフラグを見ているのだと思います。
// たぶんこんな感じ public static bool operator ==(UnityEngine.Object lhs, bool rhs) { bool objNull = ((lhs == null) || lhs.isDestroyed); return (!objNull == rhs); }
実際に問題になるケース
この偽装nullはほとんどの場合うまく動いていて問題を起こさないのですが、かなり稀なケースにおいて問題になることがある (あった) ので、それらを紹介したいと思います。
ケース① ??演算子を使ったとき
??演算子は左辺がnullなら右辺を返す演算子です。次のコードなら、普段はaを返し、aがnullのときだけbを返します。
var c = a ?? b;
??を使うとコードが簡潔になって便利なのですが、このときもしaが「偽装null」だと、bではなくaが返されてしまいます。??の内部では「obj == null」ではなく「object.ReferenceEquals(obj, null)」としてnull判定しているのだと思われます。
ケース② ジェネリック内でnull判定をしたとき
次のようなコードに「偽装null」を渡すとfalseが返ってきます。
static bool IsNull<T>(T obj) where T : class { return obj == null; }
T固有の==が実行され問題なく動きそうな気がしますが、すべてのTで共通の (たぶんSystem.Objectの) ==が実行されてしまうようです。
Tに制約をつければ、UnityEngine.Objectに定義された==が呼ばれ、問題なく動きます。
static bool IsNull<T>(T obj) where UnityEngine.Object { return obj == null; }
ただし、これだとTにUnityEngine.Object以外を渡せず不便です。どちらも渡せて、かつ見かけ上のnullを判定したい場合は、次のように条件分岐する必要があります。
static bool IsNull<T>(T obj) where T : class { var unityObj = obj as UnityEngine.Object; if (!object.ReferenceEquals(unityObj, null)) { return unityObj == null; } else { return obj == null; } }