System.NullReferenceExceptionはなぜ起こる?

2023年4月26日

プログラムを書いていると時々Nullエラーに遭遇します
特にUnityでは、参照忘れのエラーとして頻繁に出現しているでしょう

ここでは、なるべく短いC#のコードでこのエラーを再現し、どのようなことが起こっているのかを見ていきましょう

学習の条件

一度クラスやインスタンスの作成について学んだことがあることを前提とします

参照先がないコード

次のコードはエラーになります
実行しようとしてもエラーになります
なぜでしょうか?

class Program
{
    private static void Main(string[] args)
    {
        Player player;

        Console.WriteLine(player.hp);

    }
}

class Player
{
    public int hp = 10;
}

Visual Studio for Macでの画面

では、次の行を追加すればどうでしょう

player = new Player();

問題なく動作しましたね(10と表示)

では、次のようにしたらどうでしょう

player = null;

Visual Studio for Macでの画面

明らかなエラーにはなりませんし、実行もできます

ただし、次のエラーが出ます

System.Null Reference Exception
Object reference not set to an instance of an object

ローカル表示のウィンドウで、playerの値を確認すると(null)となっていますね

これはわかりやすいエラーコードです

図で表すと次のようですね

実は、newキーワードは、メモリの参照先を割り当てる仕事をしているのです

なぜUnityでは分かりづらい?

Unityでは、管理されたゲームオブジェクトとコンポーネントは、プログラマーが

new [クラス名]

でインスタンスを割り当てることを基本的にしません
エンジン内部でインスタンスを割り当てているからです

イメージ的には次のようなコードを参考にするといいでしょう

Unityイメージコード

class Program
{
    private static void Main(string[] args)
    {
        Player player;

        player = Instantiate();

        Console.WriteLine(player.hp);

    }

    static Player Instantiate()
    {
        return new Player();
    }
}

class Player
{
    public int hp = 10;
}

C#の時と同じように

return new Player();

のところを

return null;

に置き換えてみると、nullエラーになります
Unityの場合、プログラマに見えているところが、Mainメソッドだけと考えれば、エラーを追及するのが難しいことがわかります

コードの説明

正常コードの場合

Unityイメージコードをベースに、Instantiateメソッドのブロックは、次のようになっている

return new Player();

このC#コードは、Playerというクラスを定義し、それを使用してオブジェクトを生成する方法を示しています。コードの中で、Instantiate()という静的メソッドが定義され、このメソッドは新しいPlayerオブジェクトを生成して返します。Main()メソッドは、Instantiate()メソッドを呼び出してPlayerオブジェクトを取得し、そのオブジェクトのhpプロパティをコンソールに出力します。

Playerクラスは、hpというpublicな整数型フィールドを持っています。このフィールドは、インスタンス化された各Playerオブジェクトに対して、初期値として10が割り当てられます。

Main()メソッドの冒頭では、Player型のplayer変数が宣言され、初期化されていません。次に、player変数にInstantiate()メソッドを呼び出すことで生成されたPlayerオブジェクトが割り当てられます。最後に、playerオブジェクトのhpフィールドがコンソールに出力されます。

このコードを実行すると、コンソールに10という値が表示されます。

使っているデザインパターンは?

このコードは、明示的にどのデザインパターンを使っているわけではありません。ただ、基本的なオブジェクト指向プログラミングの原則に基づいています。

Playerクラスは、hpというフィールドを持ち、これはint型の値で初期値が設定されています。また、Instantiate()メソッドは、新しいPlayerオブジェクトを生成して返すシンプルなファクトリメソッドのように見えます。しかし、これらは明示的なデザインパターンではなく、単にオブジェクト指向プログラミングの基本原則に基づいています。

ただし、Instantiate()メソッドがファクトリメソッドのパターンに類似しているため、このコードはファクトリメソッドのパターンの一部として考えることができます。また、このコードが示しているように、オブジェクトの生成と初期化を専門に担当するファクトリメソッドは、オブジェクト指向プログラミングでよく使われる慣習です。

エラーコードの場合

正常時との違いは、メソッドの戻り値が次のようになっている点です

return null;

このC#コードは、Playerというクラスを定義し、それを使用してオブジェクトを生成する方法を示しています。コードの中で、Instantiate()という静的メソッドが定義され、このメソッドはnullを返します。Main()メソッドは、Instantiate()メソッドを呼び出してPlayerオブジェクトを取得し、そのオブジェクトのhpプロパティをコンソールに出力します。

Playerクラスは、hpというpublicな整数型フィールドを持っています。このフィールドは、インスタンス化された各Playerオブジェクトに対して、初期値として10が割り当てられます。

Main()メソッドの冒頭では、Player型のplayer変数が宣言され、初期化されていません。次に、player変数にInstantiate()メソッドを呼び出すことで生成されたnullが割り当てられます。最後に、playerオブジェクトのhpフィールドがコンソールに出力されます。

このコードを実行すると、NullReferenceExceptionがスローされ、実行時エラーが発生します。playerオブジェクトがnullであるため、そのフィールドであるhpにアクセスすることができず、例外がスローされます。

このコードは、実行時エラーを発生させるためのデモンストレーションとして機能しています。Instantiate()メソッドがnullを返すように定義されているため、実際にはPlayerオブジェクトが生成されていないため、player変数にはnullが割り当てられます。しかし、Main()メソッドではplayer変数を使用して、Playerオブジェクトのフィールドにアクセスしようとしているため、実行時エラーが発生します。

Unityにさらに近づける

class Program
{
    // Startメソッドのシイミュレート
    private static void Main(string[] args)
    {
        Player player;

        // ゲームオブジェクトの取得のシミュレート
        player = GameObject.Find();

        // 取得したオブジェクトのプロパティ(インスペクターの値)を確認
        Console.WriteLine(player.hp);

    }

}

// GameObject型のシミュレート(ヒエラルキーに表示されるもの)
public class GameObject
{
    // コンストラクタ(playerフィールドの初期化
    static GameObject()
    {
        // このコード行をコメントアウトするとNullエラーになります
        player = new Player();
    }

    // ゲームオブジェクト取得のシミュレート(本来はゲームオブジェクト型を取得)
    static Player player;

    // Findメソッドでゲームオブジェクト取得のシミュレート
    public static Player Find()
    {
        return player;
    }
}


public class Player
{
    public int hp = 10;
}

C#,Unity

Posted by hidepon