Unity で開発していると MissingReferenceException に悩まされることが多々あると思います。再現性が低いととても厄介です。これは、Unity オブジェクトのライフサイクルを意識することで抑制できます。
今回は MissingReferenceException が発生しやすい 3つのケースと、MissingReferenceException を発生させる Destroy されたオブジェクトの正体を考察してみます。
Case. 1
public class Test : MonoBehaviour {
public class InternalScript : MonoBehaviour {}
IEnumerator Start() {
var go = new GameObject("InternalObject");
var script = go.AddComponent<InternalScript>();
Debug.Log("script.enabled: " + script.enabled);
GameObject.Destroy(go);
Debug.Log("script.enabled: " + script.enabled);
yield return null;
Debug.Log("script.enabled: " + script.enabled);
}
}
script.enabled: True
script.enabled: False
MissingReferenceException: The object of type 'InternalScript' 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.
最もよく見るケースで、3回目の script.enabled
で MissingReferenceException が発生しています。GameObject を Destroy すると、参照が残っていようが次のフレームにはアタッチされている Component インスタンスも含め、全てのオブジェクトが破棄されます。
エラーメッセージには「null チェックするか Destroy するなよ」とありますので、メンバ変数にアクセスする前に、null チェックを入れることで解決できます。
比較的意識しやすく、対処もしやすいので、あまり大きな問題にはならない。
Case. 2
public class Test : MonoBehaviour {
public class InternalScript : MonoBehaviour {
public IEnumerator Run() {
while (true) {
Debug.Log("this: " + this);
Debug.Log("this.enabled: " + this.enabled);
yield return new WaitForSeconds(1.0f);
}
}
}
IEnumerator Start() {
var go = new GameObject("InternalObject");
var script = go.AddComponent<InternalScript>();
StartCoroutine(script.Run());
GameObject.Destroy(go);
}
}
this: InternalObject (InternalScript)
this.enabled: True
this: null
MissingReferenceException: The object of type 'InternalScript' 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.
こちらは、2回目の this.enabled
で MissingReferenceException が発生しています。 Run
関数の中では自分自身のメンバにアクセスしているので意識されにくいですが、 this
自体が null を返しているのが分かります。
このケースでは StartCoroutine
を実行する際に、コルーチンを実行するオブジェクトが適切かを意識しなければなりません。
上記のコードでは、自身を参照する InternalScript
のコルーチンを Test
オブジェクトで実行しているため、 InternalScript
オブジェクトが Destroy されても Test
オブジェクトはコルーチンを実行し続けます。
InternalScript
を下記のように実装するのが、間違った使い方をされずにすむでしょう。
public class Test : MonoBehaviour {
public class InternalScript : MonoBehaviour {
public void Run() {
StartCoroutine(InternalRun());
}
IEnumerator InternalRun() {
while (true) {
Debug.Log("this: " + this);
Debug.Log("this.enabled: " + this.enabled);
yield return new WaitForSeconds(1.0f);
}
}
}
IEnumerator Start() {
var go = new GameObject("InternalObject");
var script = go.AddComponent<InternalScript>();
script.Run();
GameObject.Destroy(go);
}
}
Case. 3
public class Test : MonoBehaviour {
public class InternalScript : MonoBehaviour {
public void Call() {
Debug.Log("this.enabled: " + this.enabled);
}
}
IEnumerator Start() {
var go = new GameObject("InternalObject");
var script = go.AddComponent<InternalScript>();
var func = new Action(script.Call);
func();
GameObject.Destroy(go);
func();
yield return null;
func();
}
}
script.enabled: True
script.enabled: False
MissingReferenceException: The object of type 'InternalScript' 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.
このケースでは、3回目の this.enabled
で MissingReferenceException が発生しています。
これを防ぐには呼び出し側で Delegate のインスタンスが生きているかを次のように確認します。
public class Test : MonoBehaviour {
public class InternalScript : MonoBehaviour {
public void Call() {
Debug.Log("this.enabled: " + this.enabled);
}
}
public class Event {
Action callback;
public Event(Action callback) {
this.callback = callback;
}
public void Call() {
var component = callback.Target as MonoBehaviour;
if (component != null)
callback();
}
}
IEnumerator Start() {
var go = new GameObject("InternalObject");
var script = go.AddComponent<InternalScript>();
var ev = new Event(script.Call);
ev.Call();
GameObject.Destroy(go);
ev.Call();
yield return null;
ev.Call();
}
}
Delegate.Target
には、その Delegate のインスタンスオブジェクトが参照されているので、こいつを null チェックします。
ポイントは MonoBehaviour にキャストした上で null チェックを行うことです。
理由は次の項で説明します。
このままでは無名関数を Delegate として渡しても絶対実行されないので Event
を無名関数でも使えるようにする為には次のようにします。
public class Event {
Action callback;
MonoBehaviour instance;
bool isComponent;
public Event(Action callback) {
this.callback = callback;
this.instance = callback.Target as MonoBehaviour;
this.isComponent = instance != null;
}
public void Call() {
if (!isComponent || instance != null)
callback();
}
}
Destroy された変数の中身は null ではない
※ここからは私の考察であり、実際の実装とは異なる場合があります。この辺りの公開情報をご存じであれば、ご教授頂けると幸いです。
ここまでは、Destroy されたかどうかを null チェックで判断してきたが、Destory されたオブジェクトは本当に null なのでしょうか。
次のコードを見てみましょう。
public class Test : MonoBehaviour {
public class InternalScript : MonoBehaviour {
public string whoami = "I am an InternalScript.";
}
IEnumerator Start() {
var go = new GameObject("InternalObject");
var script = go.AddComponent<InternalScript>();
GameObject.Destroy(go);
yield return null;
Debug.Log("script: " + script);
Debug.Log("script.whoami: " + script.whoami);
Debug.Log("script.enabled: " + script.enabled);
}
}
script: null
script.whoami: I am an InternalScript.
MissingReferenceException: The object of type 'InternalScript' 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.
script
インスタンスは null を返しているが、script.whoami
は正常にアクセスできています。しかし、次の script.enabled
は MissingReferenceException が発生しています。
この MissingReferenceException は Unity で定義されている例外で C# の null参照エラーは NullReferenceException です。これは、Destory されたオブジェクトは null のように振る舞うだけで実際には残っており、継承されたメンバにアクセスした際に MissingReferenceException を throw していることになります。
では何が Destory されたのか。
Unity には2系統のメモリマネージャ(GC)が存在します。mono と Unity です。mono は C# 上のマネージドメモリを管理し、Unity はネイティブコードのメモリを管理します。(C/C++ で new/delete されているようなもの。)
C# コード上で、Unity オブジェクトを生成した際、mono のメモリ上で変数が生成されると同時に、ネイティブコード上にもメモリが確保(こちらが実態)され、C# 上のオブジェクトはネイティブコード上のインスタンスを参照します。
Destory されると、ネイティブ側のメモリが破棄され、C# 側のネイティブオブジェクトへの参照が失われると、null として振る舞うようになり、ネイティブオブジェクトを参照していたメンバにアクセスされると MissingReferenceException を throw します。
null としての振る舞いは ToString(...)
や == operator
をオーバーライドすることで表現されています。
(object)component == null
や component ?? go.AddComponent<..>()
が意図した動作を行わない原因です。
ネイティブコードへの参照を失った C# 側のオブジェクトは mono のメモリに残り続け、mono の GC スケジュールにより破棄されます。