echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

Listに参照型の要素を追加する。(値型と参照型の違い)

「値型」と「参照型」

c#に限らず、プログラムをやると出てくる「値型」と「参照型」。

ややこしい部分であり、特に「参照型」はバグの原因になることも多いですので、実験結果も兼ねてメモ。

言い換えれば、こういうこと

僕が初学者の人に「値型と参照型の違い」について質問された時、下記の説明をします。

  • 値型(の代入):ファイルの「コピー&ペースト」
  • 参照型(の代入):ファイルの「ショートカットの作成」

前者の場合、元のファイルの内容をいくら変えようと、コピペ先のファイルの内容は変わりません。

でも後者の場合、作成元のファイルの内容を変えれば、ショートカットをダブルクリックして開いたファイルの内容も変わっています。

なので、上記の説明を借りると、

  • 値型:ファイルの「内容」(=値)をコピーする。
  • 参照型:ファイルの「リンク先」(=格納アドレス)をコピーする

と考えると、多少わかりやすいかも。

実際のコードで説明(クラスと構造体)

とはいえ「百聞は一見に如かず」なので、実験用のコードを下記に記載しました。 なおここでは、「似て非なるもの」としてよく例に出される、

  • クラス(=参照型)
  • 構造体(=値型)

を用いて説明します。
ちなみに、クラスと構造体の違いとしては、下記の通り。

処理 クラス 構造体
参照型 値型
フィールド宣言と同時に初期化 ×
引数無しコンストラクタの使用 ×
デストラクタの使用 ×
処理時間 遅い 速い

※「処理時間」に関しては、「大量のデータを処理した場合の速さの比較」なので、別に「クラスは一概に処理が遅い」わけではない。

参照:【C#】構造体の使い方

public partial class Form1 : Form
{
    struct PREF
    {
        public string pref1;
        public string pref2;
        public string pref3;
        public string pref4;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        var prefs = new PREF();
        var list1 = new List<PREF>();
        var list2 = new List<List<PREF>>();

        prefs.pref1 = "愛知";
        prefs.pref2 = "岐阜";
        prefs.pref3 = "三重";
        prefs.pref4 = "静岡";

        list1.Add(prefs);
        list2.Add(list1);

        // 1

        prefs.pref1 = String.Empty;
        prefs.pref2 = String.Empty;
        prefs.pref3 = String.Empty;
        prefs.pref4 = String.Empty;

        // 2

        list1.Clear();

        // 3

        prefs.pref1 = "徳島";
        prefs.pref2 = "高知";
        prefs.pref3 = "愛媛";
        prefs.pref4 = "香川";

        list1.Add(prefs);

        // 4

        list2.Add(list1);

        // 5
    }
}

上記のコードの1~5の箇所で「list1」と[list2]のpref1~pref4の値をチェックすると、下記の違いがあります。

  • 1&2

    • list1
      • [0]
        • [pref1] = "愛知"
        • [pref2] = "岐阜"
        • [pref3] = "三重"
        • [pref4] = "静岡"
    • list2
      • [0]
        • [0]
          • [pref1] = "愛知"
          • [pref2] = "岐阜"
          • [pref3] = "三重"
          • [pref4] = "静岡"
  • 3

    • list1
      • 要素無し
    • list2
      • [0]
        • 要素無し
  • 4

    • list1
      • [0]
        • [pref1] = "徳島"
        • [pref2] = "高知"
        • [pref3] = "愛媛"
        • [pref4] = "香川"
    • list2
      • [0]
        • [0]
          • [pref1] = "徳島"
          • [pref2] = "高知"
          • [pref3] = "愛媛"
          • [pref4] = "香川"
  • 5

    • list1
      • [0]
        • [pref1] = "徳島"
        • [pref2] = "高知"
        • [pref3] = "愛媛"
        • [pref4] = "香川"
    • list2
      • [0]
        • [0]
          • [pref1] = "徳島"
          • [pref2] = "高知"
          • [pref3] = "愛媛"
          • [pref4] = "香川"
      • [1]
        • [0]
          • [pref1] = "徳島"
          • [pref2] = "高知"
          • [pref3] = "愛媛"
          • [pref4] = "香川"

動作の説明

  • 1:特に問題ないと思います。

  • 2:元の「prefs」の全フィールドの値を「String.Empty」にしていますが、prefsは(値型の)構造体なので、元のprefsが変更されても、list1の内容は変わりません。

  • 3:「list1.clear()」をしているので「list1」の要素はクリアされますが、list1は(参照型の)クラスなので、1の直前にlist1を追加した「list2」も、「list1」の変更の影響を受けます。 (ただし、list1自体が無くなったわけではないので、list1の参照自体は残っています。(=list2.Countの値は1のまま))

  • 4:ここが一番予想しにくい箇所。 「list1」に要素が変更されているのは当然ですが、「list2[0]」要素は「list1の参照先」なので、「list1」の要素が変更されれば、list2[0]の内容も直接影響を受けます。

  • 5:4でlist2[0]の要素が変更されましたが、list2.add()が実行されたので、list2[1]に新たにlist1の内容が追加されます。 結果、全く同じリスト(=list1)が2つ入っている、という結果になります。

と、参照型の代入は、想定外の動作をする場合があるので、気を付けないと、思わぬバグの原因になったりします。

回避策

簡単に言えば、「クラスの値のみコピーしたリストを追加する」処理を実施すればOKです。 やり方ですが、ソースのlist2.add(list1)の箇所を、

list2.add(new List<List<PREF>>(list1))

のように、

  • コンストラクタの引数に値コピー元のリストを設定して新規作成したたListクラス

を追加すれば、元のリスト(list1)の値が変更されようと、list2の内容は変わりません。

クラスと構造体の使い分け

最後になりますが、たまに参考書や一部のブログで、

迷ったら、クラスを使っておけば問題ない

という記載を見かけますが、何も考えずにクラスを使うと、上記のような思わぬバグを引き起こすので、要注意です。

個人的には、

  • クラス:色々な機能を付ける、拡張性を持たせる
  • 構造体:単に「値の受け皿」として使用する

として使い分けするのが良いかと思います。

以上、今回は長文になってしまいました。