【ゲーム開発のためのC#入門講座・基礎強化編】参照型の引数に気を付けよう【#8】

2022-01-093.0_C#基礎強化編

おさらい

引数という単語、皆さん覚えていますか?

関数の入力で定義する変数のことを引数またはパラメータと呼ぶんでしたね。

もしうろ覚えの方がいたら基礎拡張編の関数回を見直してみましょう。

この引数、値型と参照型では挙動が異なります

今回はこの違いを学んでいきましょう。

引数には何が設定される?

引数には設定元の変数やデータの値が代入されます。

そして参照型の演習で少し解説しましたが、 代入とはスタックメモリの値を代入する機能です。

ここで「あれ、ということは……」と思った方、とても鋭いですね。

そう、値型の場合はスタックメモリに直接値が保存されています。

一方、参照型の場合はスタックメモリにアドレスが保存されてます。

それを代入するとどうなるか。せっかくなので、具体例を交えながらメモリの動きを追いかけて確認してみましょう。

値型を引数とした場合

まずは値型を引数とした場合です。

下記プログラムを例とします。

public class Hello{
    public static void Main(){
        int number = 0;
        PlusOne(number);
        System.Console.WriteLine(number);
    }
    public static void PlusOne(int parameter)
    {
        parameter += 1;
    }
}
出力エリア

0

このプログラムでは関数「PlusOne」を実行する時、下記のように引数が渡されます。

変数スタックメモリのアドレス
numberスタックメモリアドレスA0
↓値を代入
parameterスタックメモリアドレスB0

それから関数内の処理が実行され、

変数スタックメモリのアドレス
numberスタックメモリアドレスA0
parameterスタックメモリアドレスB0 + 1 = 1

最後に関数終了により、関数内のメモリが解放されます。

変数スタックメモリのアドレス
numberスタックメモリアドレスA0
parameterスタックメモリアドレスB1

なので、値型の場合は元の変数が関数内処理の影響を受けることはないんですね。

参照型を引数とした場合

一方で、参照型の場合はスタックメモリにアドレスが保存されています。

先程とほとんど同じ構成の下記プログラムを例に考えてみましょう。

public class Hello{
    public static void Main(){
        int[] numbers = {
            0,
            0
        };
        PlusOne(numbers);
        System.Console.WriteLine(numbers[0]);
    }
    public static void PlusOne(int[] parameter)
    {
        parameter[0] += 1;
    }
}
出力エリア

1

このプログラムでは関数「PlusOne」を実行する時、下記のように引数が渡されます。

変数スタックメモリのアドレス
numbersスタックメモリアドレスAヒープメモリアドレスA
↓値を代入
parameterスタックメモリアドレスBヒープメモリアドレスA
ヒープメモリのアドレス
ヒープメモリアドレスA0, 0

関数内で変数を読み書きする場合、

  1. 格納されたヒープメモリのアドレスを参照し、
  2. アドレスの先にある実データに対して読み書き

という手順で処理が実行されます。

変数スタックメモリのアドレス
numbersスタックメモリアドレスAヒープメモリアドレスA
parameterスタックメモリアドレスBヒープメモリアドレスA(①参照)
ヒープメモリのアドレス
ヒープメモリアドレスA(②参照先)0 + 1 = 1(③更新), 0

最後に関数終了により、関数内のメモリが解放されます。

変数スタックメモリのアドレス
numbersスタックメモリアドレスAヒープメモリアドレスA
parameterスタックメモリアドレスBヒープメモリアドレスA
ヒープメモリのアドレス
ヒープメモリアドレスA1, 0

つまり、参照型の場合は値型と違って元の変数が関数内処理の影響を受けるのです。

だから出力結果が値型の時と異なり「1」になったんですね。

これが基礎拡張編の最終課題の時に紹介した、訳が分からない事象の答えです。

Unityでの活躍ポイント

これから皆さんはオブジェクト指向を学び、独自のデータ型をたくさん作っていくことになります。

勇者型とか、魔法使い型とか、本当に自由にプログラムを作れるようになります。

ワクワクしますね!

ただし、この独自のデータ型はすべて参照型に該当します。だから値型と参照型の挙動の違いを、その事象と理由の両面から理解しておくことが必要だったんですね。

メモリの話はなかなか難しいのでここまで大変だったかと思います。が、皆さんがメモリの話をひとつひとつ地道に学習してきてくださったおかげで、オブジェクト指向と戦うための堅牢な土台作りは無事完了しました。

少しはやいですが、お疲れ様でした&ありがとうございました!

実践演習

それでは実際に、値型と参照型の違いを踏まえて、プログラムの挙動を追いかけてみましょう。

演習①

下記のプログラムを実行した際、出力エリアに表示される値を予想してください。

public class Hello{
    public static void Main(){
        int number = 0;
        FuncA(number);
        FuncB(number);
        System.Console.WriteLine(number);
    }
    public static void FuncA(int parameter)
    {
        parameter += 1;
    }
    public static void FuncB(int parameter)
    {
        parameter += 1;
    }
}

演習②

下記のプログラムを実行した際、出力エリアに表示される値を予想してください。

public class Hello{
    public static void Main(){
        int[] numbers = {
            0,
            0
        };
        FuncA(numbers);
        FuncB(numbers);
        System.Console.WriteLine(numbers[0]);
    }
    public static void FuncA(int[] parameter)
    {
        parameter[0] += 1;
    }
    public static void FuncB(int[] parameter)
    {
        parameter[0] += 1;
    }
}

答え合わせ

演習①の答え

出力エリア

0

演習②の答え

出力エリア

2

まとめ

  • 値型を引数とした場合、元の変数が関数内処理の影響を受けることはない
  • 参照型を引数とした場合、元の変数が関数内処理の影響を受ける

それでは、今回もお疲れ様でした!

また次の記事でお会いしましょう!

Posted by 夕目紅