という話。
以前、友人に上手く説明できなかったので、自分なりに色々検証コードを書いてみました。
リポジトリはこちら https://github.com/satanabe1/UnityNullObjects
nullチェク、しますよね。
if (obj == null) return;
よくあるパターンです。
Unityで == null
が成り立つ状況って、ざっくり次の二つが挙げられます。
- もともとnullが代入されていた (nullでないものが代入されなかった)
- Destroyされた
当たり前なことしか書いてないですが、この2つ目が厄介です。
以下にぐだぐだと書いていきます。
Destroyされてもすぐには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してもすぐに消えるわけじゃないぜ。っと公式ドキュメントにも書いてあった気がします。納得です。
Destroyからの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になったはずのdestroied
とdestroiedImmediate
から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ではない。
という動きですね。
どうしてこうなるのか?
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のお作法かもしれません。