読者です 読者をやめる 読者になる 読者になる

Nobollel開発者ブログ

Nobollelのエンジニアが、UnityやCocos2d-xの旬な情報・技術を紹介します。

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");
	}
}

何が起きているのか

この記事にもあるように、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;
	}
}