🐷

【Unity】憎きNull Reference Exception をなくす方法まとめ

2024/12/18に公開

Null Reference Exceptionが憎い。とんでもなく。

Null Reference Exception、ウザいですよね!一つでも多くぶっ潰したい!

Unityを勉強していく中で学んだ対策を紹介していきます!
まだまだ未熟なので、間違っているところがあればビシバシ言っていただけると!


対象読者について

Unityでゲーム開発を始めたばかりの方から、中級者ぐらいの方までを想定しています。基本的なC#の知識やUnityのInspectorの使い方を知っているとスムーズに読めるはずです!


1. Null Reference Exception とは

Unityでは、あるオブジェクトや値がnullのときにそれを参照しようとして起こるエラーです。例えば、クラスのプロパティやメソッドが存在しない場合に発生します。

簡単に言うと、宅配便を届けに行ったのに、住人がいなくて荷物が受け取れない!そんな感じです。

2. なぜ開発においてNull Reference Exceptionに対面したくないのか

2.1. 何がnullなのか教えてくれないときがあるから

例えば、あるクラスのpublicプロパティを参照してExceptionが起こった場合、そのクラスのインスタンスがnullなのか、プロパティ自体がnullなのかわかりません。追加でデバッグが必要で、これが手間なんですよ。

private void Start()
{
    Debug.Log(myInstance.myString.Length); // NullReferenceException発生
    // myInstanceがnull?
    // myStringがnull?
}

2.2. 他のスクリプトの実行に影響を及ぼしてバグの所在がわからなくなる

Null Reference Exceptionはスクリプト内で発生するとその時点で処理が中断されるため、後続のロジックにも影響を及ぼします。その結果、バグの原因特定や解析がさらに難しくなることも。

2.3. スクリプト実行順によって起こったり起こらなかったりする

スクリプト実行順や依存関係の影響で、エディタでは問題なく動いていたのに、ビルド後にエラーが出るなんてことも。

スクリプト実行順は基本的にいじらないのがベストだと思っています!


3. Null Reference Exceptionの防ぎ方

3.1. Nullガードを作る

これ、地味だけど実際一番効果的。エラーログで何が問題だったのかもすぐわかります。

パターン1: Nullチェック

if (target == null)
{
    Debug.LogError("[Error] targetがnullです。初期化の確認をしてください");
    return;
}

パターン2: 確実な方法でインスタンスを探す

//Unity 6ではFindObjectByTypeは非推奨、FindFirstObjectByTypeなどを使うと良い

target = Object.FindFirstObjectByType<Target>();

if (target == null)
{
    Debug.LogError("[Error] 必要なインスタンスが見つかりません");
    return;
}

パターン3: インスタンスが初期化されるまで待つ

コルーチンを使用する場合:

private IEnumerator WaitForInstance()
{
    yield return new WaitUntil(() => target != null);
    target.DoSomething();
}

//async メソッドでおしゃれに書いてみてもOK!
private async Task WaitForInstanceAsync()
{
    while (target == null)
    {
        await Task.Yield();
    }
    target.DoSomething();
}


3.2. SerializeFieldを使用して必要なインスタンスを渡す

Inspectorで手動設定することでエラーを減らせます。ただし、スクリプトが増えるとその分手間も増えるのがデメリット。

例:

[SerializeField] private GameObject target;

private void Start()
{
    if (target == null)
    {
        Debug.LogError("[Error] targetが設定されていません");
    }
}


3.3. RequireComponentで存在を保証する

同じGameObjectに存在するコンポーネントが必要な場合に効果的な方法。

例:

[RequireComponent(typeof(Rigidbody))]
public class MyComponent : MonoBehaviour
{
    private Rigidbody rb;

    void Awake()
    {
        rb = GetComponent<Rigidbody>();
        if (rb == null)
        {
            Debug.LogError("[Error] Rigidbodyが見つかりません");
        }
    }
}


3.4. 設計を見直す (依存性の注入のすすめ)

3.4.1. GetComponentを呼ぶ場所を限定する

あちこちでGetComponentを呼び出していると、スクリプト実行順や生成タイミングでNullになることがあります。特定のクラスで必要なコンポーネントをまとめてGetし、それを分配する形にすると同期がとれ、Nullの原因も特定しやすくなります。

private void Initialize()
{
    rb = GetComponent<Rigidbody>();
    col = GetComponent<Collider>();
    if (rb == null || col == null)
    {
        Debug.LogError("[Error] 必要なコンポーネントが見つかりません");
    }
}

3.4.2. コンストラクタ(っぽいもの)を使う

MonoBehaviour以外のクラスではコンストラクタを使ってインスタンスを渡すのがオススメ。

public class MyService
{
    private readonly Rigidbody rb;
    private readonly Collider col;

    public MyService(Rigidbody rb, Collider col)
    {
        this.rb = rb;
        this.col = col;

        Debug.Log("[Info] MyServiceが初期化されました");
    }
}

MonoBehaviourのクラスはコンストラクタが使えないので、publicメソッドで代用できます。

public class MyInitializer : MonoBehaviour
{
    private Rigidbody rb;
    private Collider col;

    public void Initialize(Rigidbody rb, Collider col)
    {
        this.rb = rb;
        this.col = col;
    }
}

Zenject(Extenject)という依存性注入フレームワークを使えば、これらの操作がさらに簡単になります。詳細は以下のリンクをご覧ください。

https://light11.hatenadiary.com/entry/2019/02/16/225023

Discussion