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

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

参照型って?

命令を実行するまでメモリサイズがわからないデータ種類をまとめて参照型と呼びます。

参照型はデータを作成する時にnewというキーワードをつけます。これは「新しくデータを作ってその分のヒープメモリを確保しろ」という命令なんですね。

// ↓配列もnewでデータを作成する
int[] scores = new int[2];

「あれ、配列ってnewとか記述しなくても直接データ定義できなかったっけ?」

と思った方、あれは「new」という命令を省略したものだったりします。

int[] scores = {
    10,
    20,
    30
};

// ↑は↓の省略形
int[] scores = new int[3];
int[0] = 10;
int[1] = 20;
int[2] = 30;

同じように、実はstring型も「new」を省略しています。

string text = "hoge";

// ↑は↓の省略形
string text = new string(new char[] {'h', 'o', 'g', 'e'});

このような書き方をいちいちしていたら非常にだるいんですよね。

なので簡潔に書けるようになっていますが、内部的にはちゃんと「new」という命令を受けて処理が実行されています。

参照型って何を参照してるの?

参照型って何かまたややこしい名前やなー、って感じですよね。いっそ「メモリサイズ変動型」とかって言った方がまだ直感的でわかりやすいような気もします。

そもそも何を参照しているのでしょう?

この疑問の答えをこれから解説します。

ヒープメモリは命令を受けてから必要な量だけメモリを確保し、命令を受けてから解放するんでしたね。

そのため、必ずしも後ろにだけスペースがあるとは限りません。使わなくなって解放されたメモリ領域が使えそうだったら、どんどんそこに割り当てていくことになります。

空きスペースを有効活用するのはいいことなのですが、そのせいでどこにどのデータがあるのかわからないという問題も発生します。

そこで、ヒープメモリのこの場所に格納しましたよ、というデータの格納場所を示す値を別に保存しておくことにしました。

この値のことを住所になぞらえてアドレスといいます。

このアドレスは最大サイズが明確なので、スタックメモリに保存されます。

つまり、参照型のデータはアドレスがスタックメモリに、実データがヒープメモリに保存されるというちょっと特殊な管理方法になっているのです。

そのため、参照型のデータに対して読み書きする場合は、

  1. スタックメモリに保存されたヒープメモリのアドレスを参照し、
  2. 1.のアドレスに格納された実データを操作する

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

うーん、効率は確かにいいんだろうけど、非常にややこしい!😔

とはいえ何を参照している型なのか、謎は解けましたね。

ちなみに、値型はこの参照型の対となる表現になっていて、スタックメモリにアドレスではなく直接値を格納するから値型といいます。

「ふむふむ……ってあれ、今までメモリの解放命令なんて1回も出した覚えないけど、ずっと文字列とか配列とか使ってるよ? 大丈夫?」

と思った方、鋭いですね。そこについては次回解説します。

Unityでの活躍ポイント

今回学んだことは、知らないと「は?」となる現象を理解するのにとても重要です。

実際に「は?」となる事象と理由を解説するので、詳細は実践演習をご確認下さい。

Unityでゲーム作りをする時に発生する思わぬバグを回避したり、原因を理解することができるでしょう。

実践演習

それでは、実際に参照型の挙動を確認してみましょう。

演習①参照型のデータを代入してみよう

下記プログラムを実行すると、出力エリアにどう表示されるか予想してみてください。

public class Hello{
    public static void Main(){
        int[] A = {
            5,
            10
        };
        
        int[] B = A;
        B[0] += 10;
        
        // ↓この2行の命令でどう表示されるか想像してみよう!
        System.Console.WriteLine(A[0]);
        System.Console.WriteLine(B[0]);
    }
}

演習②文字列で演習①と同じことをやってみよう

下記プログラムを実行すると、出力エリアにどう表示されるか予想してみてください。

public class Hello{
    public static void Main(){
        string A = "hoge";
        
        string B = A;
        B += "fuga";
        
        // ↓この2行の命令でどう表示されるか想像してみよう!
        System.Console.WriteLine(A);
        System.Console.WriteLine(B);
    }
}

答え合わせ

演習①の答え

出力エリア

15
15

「5」と「15」が表示されると予想した方も多いのではないでしょうか?

しかし実際にはご覧の通り、両方とも15が表示されます。

これはなぜかというと、代入とはスタックメモリの値を代入する機能だからです。

参照型の場合はスタックメモリにアドレスを保存しているんでしたね。

ということは、参照型の変数を代入すると、代入先に同じアドレスが保存されてしまうのです。

同じアドレスを参照しているということは、同じ実データを参照しているということです。つまり変数Bを更新する=変数Aを更新するのと同義になってしまうんですね。

このように、参照型の変数を代入すると同じアドレスを参照してしまうという点に注意が必要です。

めんどくさ気を付けていきましょう。

演習②の答え

出力エリア

hoge
hogefuga

えー、演習①の話と違うやんけ! と思った方、そうなんですわ……ここが文字列のややこしいところです。

公式リファレンスにこの事象の理由が書いてあります。

文字列オブジェクトは 変更不可 です。つまり、作成した文字列オブジェクトは変更できません。 文字列を変更するように見える String メソッドと C# 演算子はすべて、実際には新しい文字列オブジェクトで結果を返します。 

つまり、文字列の結合処理も実際には新しくデータを作っているのです。

B += "fuga";
// ↑は実際には↓のように新しくデータを作成し直している
B = new string(new char[] {'h', 'o', 'g', 'e', 'f', 'u', 'g', 'a'});

だから同じアドレスを読み書きしてしまうということが発生しないのです。

非常にややこしい話ですけど、この仕様のおかげでstring型は値型と同じ感覚で自由に代入できます(アドレスを気にせず使える)。頻繁に使うからこそ余計なこと考えなくて済むように、というMicrosoft側の優しさかなと勝手に思っています。

まとめ

  • 参照型とは、プログラムが命令を実行するまでサイズがわからないデータ型の総称
  • データを作成する時は「new」というキーワードを使う
  • 「参照」型と呼ばれるのは、スタックメモリに参照用のアドレスを格納しているから
  • 参照型の変数を代入すると同じアドレスを参照してしまう点に注意
  • ただしstring型だけは例外(値型と同じ感覚で使ってOK)

次回はプログラマーの救世主ガベージ・コレクションについて学習します。お楽しみに!

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

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

お借りした素材一覧

この記事では下記サイト様の素材をお借りしています。

ありがとうございました!

かわいいフリー素材集 いらすとや (irasutoya.com)

Posted by 夕目紅