1. Qiita
  2. 投稿
  3. Unity

Unityのnullはnullじゃないかもしれない

  • 70
    いいね
  • 7
    コメント
この記事は最終更新日から1年以上が経過しています。

という話。
以前、友人に上手く説明できなかったので、自分なりに色々検証コードを書いてみました。
リポジトリはこちら https://github.com/satanabe1/UnityNullObjects


nullチェク、しますよね。

if (obj == null) return;
よくあるパターンです。
Unityで == null が成り立つ状況って、ざっくり次の二つが挙げられます。

  1. もともとnullが代入されていた (nullでないものが代入されなかった)
  2. Destroyされた

当たり前なことしか書いてないですが、この2つ目が厄介です。
以下にぐだぐだと書いていきます。

Destoryされてもすぐにはnullにならない

/// <summary>
/// DestroyとDestroyImmediate
/// </summary>
public class NullTest01 : NullTestBase
{
    IEnumerator Start ()
    {
        MonoBehaviour destroied = gameObject.AddComponent<MonoBehaviour> ();
        MonoBehaviour destroiedImmediate = gameObject.AddComponent<MonoBehaviour> ();
        MonoBehaviour nullBehaviour = null;
        Destroy (destroied);
        DestroyImmediate (destroiedImmediate);
        Debug.Log ("destroied          : [" + destroied + "]");
        Debug.Log ("destroiedImmediate : [" + destroiedImmediate + "]");
        Debug.Log ("nullBehaviour      : [" + nullBehaviour + "]");
        yield break;
    }
}

/****** ログ *****
destroied : [Runner (UnityEngine.MonoBehaviour)]
destroiedImmediate : [null]
nullBehaviour : [] 
***** ******/

ログの一行目、Destroy済みのdestroiedが、destroied : [Runner (UnityEngine.MonoBehaviour)] と出てますね。
DestroyImmediateされた方は綺麗さっぱり null になってますね。
まぁ、Destroyしてもすぐに消えるわけじゃないぜ。っと公式ドキュメントにも書いてあった気がします。納得です。

Destoryからの1フレーム後ならちゃんとnull (?)

/// <summary>
/// DestroyとDestroyImmediateとフレーム跨ぎ
/// </summary>
public class NullTest02 : NullTestBase
{
    IEnumerator Start ()
    {
        MonoBehaviour destroied = gameObject.AddComponent<MonoBehaviour> ();
        MonoBehaviour destroiedImmediate = gameObject.AddComponent<MonoBehaviour> ();
        MonoBehaviour nullBehaviour = null;
        Destroy (destroied);
        DestroyImmediate (destroiedImmediate);
        yield return null;
        Debug.Log ("destroied          : [" + destroied + "]");
        Debug.Log ("destroiedImmediate : [" + destroiedImmediate + "]");
        Debug.Log ("nullBehaviour      : [" + nullBehaviour + "]");
        yield break;
    }
}
/****** ログ *****
destroied          : [null]
destroiedImmediate : [null]
nullBehaviour      : []
***** ******/

Debug.Logの前にyield return null;を挟んだだけです。
直感に沿う結果です。
ただ、そろそろ気持ち悪さを感じますね。
なぜ、唯一最初からnullを代入されていたnullBehaviorだけが違う出力なのか。

なら '== null'してみよう

/// <summary>
/// 削除済みobjectとnull
/// </summary>
public class NullTest03 : NullTestBase
{
    IEnumerator Start ()
    {
        MonoBehaviour destroied = gameObject.AddComponent<MonoBehaviour> ();
        MonoBehaviour destroiedImmediate = gameObject.AddComponent<MonoBehaviour> ();
        MonoBehaviour nullBehaviour = null;
        Destroy (destroied);
        DestroyImmediate (destroiedImmediate);
        Debug.Log ("EqualsNull");
        Debug.Log ("destroied          : [" + (destroied == null) + "]");
        Debug.Log ("destroiedImmediate : [" + (destroiedImmediate == null) + "]");
        Debug.Log ("nullBehaviour      : [" + (nullBehaviour == null) + "]");
        yield return null;
        Debug.Log ("destroied          : [" + (destroied == null) + "]");
        Debug.Log ("destroiedImmediate : [" + (destroiedImmediate == null) + "]");
        Debug.Log ("nullBehaviour      : [" + (nullBehaviour == null) + "]");
        yield break;
    }
}
/****** ログ *****
EqualsNull
destroied          : [False]
destroiedImmediate : [True]
nullBehaviour      : [True]
destroied          : [True]
destroiedImmediate : [True]
nullBehaviour      : [True]
***** ******/

削除が完了すれば、ちゃんと== nullが成り立ってますね。
やっぱりみんなnullじゃん。
じゃあ、なんで出力が違ったの?ということで、

nullなオブジェクトにメソッド(GetTypeとGetComponentInParent)呼び出ししてみる

/// <summary>
/// 削除済みobjectへのメソッド呼び出し
/// </summary>
public class NullTest04 : NullTestBase
{
    IEnumerator Start ()
    {
        MonoBehaviour destroied = gameObject.AddComponent<MonoBehaviour> ();
        MonoBehaviour destroiedImmediate = gameObject.AddComponent<MonoBehaviour> ();
        MonoBehaviour nullBehaviour = null;
        Destroy (destroied);
        DestroyImmediate (destroiedImmediate);
        yield return null;
        Debug.Log ("=====  GetType  =====");
        try {
            Debug.Log ("---");
            Debug.Log ("destroied          : [" + destroied.GetType () + "]");
        } catch (System.Exception ex) {
            Debug.LogError (ex);
        }
        try {
            Debug.Log ("---");
            Debug.Log ("destroiedImmediate : [" + destroiedImmediate.GetType () + "]");
        } catch (System.Exception ex) {
            Debug.LogError (ex);
        }
        try {
            Debug.Log ("---");
            Debug.Log ("nullBehaviour      : [" + nullBehaviour.GetType () + "]");
        } catch (System.Exception ex) {
            Debug.LogError (ex);
        }
        Debug.Log ("=====  GetComponentInParent  =====");
        try {
            Debug.Log ("---");
            Debug.Log ("destroied          : [" + destroied.GetComponentInParent<Component> () + "]");
        } catch (System.Exception  nex) {
            Debug.LogError (nex);
        }
        try {
            Debug.Log ("---");
            Debug.Log ("destroiedImmediate : [" + destroiedImmediate.GetComponentInParent<Component> () + "]");
        } catch (System.Exception ex) {
            Debug.LogError (ex);
        }
        try {
            Debug.Log ("---");
            Debug.Log ("nullBehaviour      : [" + nullBehaviour.GetComponentInParent<Component> () + "]");
        } catch (System.Exception ex) {
            Debug.LogError (ex);
        }
        yield break;
    }
}
/****** ログ *****
=====  GetType  =====
---
destroied          : [UnityEngine.MonoBehaviour]
---
destroiedImmediate : [UnityEngine.MonoBehaviour]
---
System.NullReferenceException: Object reference not set to an instance of an object
  at NullTest04+<Start>c__Iterator3.MoveNext () [0x0011d] in /Users/hoge/unity/Null/Assets/NullTest04.cs:32 
=====  GetComponentInParent  =====
---
UnityEngine.MissingReferenceException: The object of type 'MonoBehaviour' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
  at (wrapper managed-to-native) UnityEngine.Component:InternalGetGameObject ()
  at UnityEngine.Component.get_gameObject () [0x00000] in <filename unknown>:0 
  at UnityEngine.Component.GetComponentInParent (System.Type t) [0x00000] in <filename unknown>:0 
  at UnityEngine.Component.GetComponentInParent[Component] () [0x00000] in <filename unknown>:0 
  at NullTest04+<Start>c__Iterator3.MoveNext () [0x0016d] in /Users/hoge/unity/Null/Assets/NullTest04.cs:39 
---
UnityEngine.MissingReferenceException: The object of type 'MonoBehaviour' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
  at (wrapper managed-to-native) UnityEngine.Component:InternalGetGameObject ()
  at UnityEngine.Component.get_gameObject () [0x00000] in <filename unknown>:0 
  at UnityEngine.Component.GetComponentInParent (System.Type t) [0x00000] in <filename unknown>:0 
  at UnityEngine.Component.GetComponentInParent[Component] () [0x00000] in <filename unknown>:0 
  at NullTest04+<Start>c__Iterator3.MoveNext () [0x001b5] in /Users/hoge/unity/Null/Assets/NullTest04.cs:45 
---
System.NullReferenceException: Object reference not set to an instance of an object
  at NullTest04+<Start>c__Iterator3.MoveNext () [0x001fd] in /Users/hoge/unity/Null/Assets/NullTest04.cs:51 
***** ******/
GetType

そう。DestroyされてnullになったはずのdestroieddestroiedImmediateからGetType()で型を拾えてしまうんです。
最初からnullを代入されていたnullBehaviourはNullReferenceException。

GetComponentInParent

一方、GetComponentInParentだと、3つともエラーになってしまうものの・・・やっぱり様子が違いますね。
やっぱり、こいつら純粋なnullじゃない?

is a してみる

MonoBehaviour nullBehaviour = null;
Destroy (destroied);
DestroyImmediate (destroiedImmediate);
yield return null;
Debug.Log("is a UnityObject");
Debug.Log ("destroied          : [" + (destroied is UnityEngine.Object) + "]");
Debug.Log ("destroiedImmediate : [" + (destroiedImmediate is UnityEngine.Object) + "]");
Debug.Log ("nullBehaviour      : [" + (nullBehaviour is UnityEngine.Object) + "]");
/****** ログ *****
is a UnityObject
destroied          : [True]
destroiedImmediate : [True]
nullBehaviour      : [False]
***** ******/

やはりnullBehaviourだけ仲間はずれです。
一見、nullと等価であり、かつ、UnityEngine.Objectでもある。そしてNullReferenceではない。
という動きですね。

どうしてこうなるのか?

スクリーンショット 2015-01-02 19.03.29.png
UnityEngine.Objectの定義をMonoDevelopで追ってみると、==演算子がオーバーライドされている事がわかります。
Object.CompareBaseObjectsメソッドの中身までは追っていませんが、おそらくDestroy済みかどうかのフラグ的なものをみて、Destroy済みならnullに対してtrueを返す・・・といった処理が書いてあるのでしょう。きっと。

したがって、 Unityオブジェクトの場合、'== null'が成り立っても本当にnullである保証はどこにもない のです。
つまり、 メモリは開放されていない

仮にnullがnullじゃなかったとして、何がいけないのでしょうか?

と言われてしまいそうですが、これ、意識の片隅に置いておいたほうがいいかもしれません。

たとえば、== nullの親戚に ??演算子があります。
'左辺がnullなら右辺を返す' という意味の演算子ですね。
この??演算子はUnityオブジェクト側でオーバーライドされていないので、破棄済みのUnityオブジェクトにたいしても、'nullではない' として扱います。事故ですね。

また、nullチェックが成り立ったからといって、そのまま変数を放置しておいたらメモリ中にUnityオブジェクトの死体が散乱するかもしれません。地獄ですね。

そしてもう一点、UnityオブジェクトをSystem.Objectにアップキャストしてからnullチェックを行うと、== nullが牙を剝いてきます。(僕はそれでヤラレました)
下記のコードと、そのログ出力をよく眺めて見て下さい。

Destroy (destroied);
DestroyImmediate (destroiedImmediate);
yield return null;
Debug.Log ("EqualsNull");
Debug.Log ("destroied                 : [" + (destroied == null) + "]");
Debug.Log ("destroiedImmediate        : [" + (destroiedImmediate == null) + "]");
Debug.Log ("nullBehaviour             : [" + (nullBehaviour == null) + "]");
Debug.Log ("(cast) destroied          : [" + (((System.Object)destroied) == null) + "]");
Debug.Log ("(cast) destroiedImmediate : [" + (((System.Object)destroiedImmediate) == null) + "]");
Debug.Log ("(cast) nullBehaviour      : [" + (((System.Object)nullBehaviour) == null) + "]");
/****** ログ *****
EqualsNull
destroied                 : [True]
destroiedImmediate        : [True]
nullBehaviour             : [True]
(cast) destroied          : [False]
(cast) destroiedImmediate : [False]
(cast) nullBehaviour      : [True]
***** ******/

これはUnity以前のC#の振る舞いですが、キャストしたことによって、==演算子のオーバーライドが効かなくなっています。
よって、たとえば、

/// nullだっときの汎用的な処理が欲しい。とか。
bool IsNull(System.Object arg)
{
   Debug.Log("nullだよ。とかログを取ったり。ごにょったり。");
   return arg == null;
}

のようなコードを書いていたら、Destroy済みのUnityオブジェクトであっても'nullではない'という結果が返って来ます。MissingReferenceExceptionさんこんにちは。

nullがnullでないnullであることをnullチェックするには

おまけです。

System.Object obj = ...
if (obj is UnityEngine.Object) {
    if ((UnityEngine.Object)obj != null) {
        // 元気なUnityオブジェクト
    } else {
        // 死んだフリしているUnityオブジェクト
    }
} else {
    if (obj != null) {
        // 普通のnullでないオブジェクト
    } else {
        // ガチnull
    }
}

こんな感じでしょうか。
まともじゃないですね。

そもそもSystem.Objectを使わない、というのがUnityのお作法かもしれません。