値型と参照型の違いを理解しよう!

[C#] 値型と参照型の違いを理解しよう!

※ 当サイトは広告を含みます。

関数(※1)を学んだことで、ついに値型参照型の違いを知ることができます。
ここではC#で最重要とも言える値型と参照型の違いを理解しましょう。

※1現在の知識を踏まえて、ここではメソッドではなく関数と呼びます。

値型と参照型の復習

まずは値型と参照型について復習しましょう。
この2つは値を保存する場所が大きなポイントになります。

値型

値型とは用意した領域に直接的に値を保存します。

値型

参照型

参照型は値を別の領域に格納し、それを示す参照値(アドレス)を使って操作します。

参照型
Tipsプログラミング言語ごとに呼び方が異なりますが、参照とポインタは殆ど同じ意味です。

値渡し

プログラムには、変数から変数、または関数に値を渡す場面がありますよね。
この時に値を渡すことを値渡しと呼びます(たぶん)。

具体的には次のようなパターンです。

  1. 変数から変数に代入
  2. 関数の引数に値を指定
  3. 関数の戻り値を利用

何気なく書いてると思いますが、この時に値はコピーされてます。
そのコピーにおいて値型と参照型は全く異なる動作をするので、これを理解する必要があります。

1番分かりやすいのが関数で値渡しする場合の例でしょう。

値型を関数に渡す場合、引数に指定した値と同じだけのサイズが新たに確保され、そこに現在の値が全てコピーされます。

値型の値渡し

つまり引数に渡すサイズが大きいほどコピーに時間を要し、その分だけ処理に遅延が発生します。
これは戻り値が値型の場合も同じです。とは言え、昨今のコンピュータでは数十byte程度は気にするレベルではないです。

加えてもう1つの重要なポイントがあります。それは値渡しで渡した変数と関数内の変数は別物ということです。
これはコピーしてるので当然のことなのですが、例え同じ名前で作った変数でも、それは同じ変数を示しません。

要は関数内で同名の変数の値を変更しても、元の値渡しで利用した変数には一切影響がないのです。
これを確認するサンプルコードがあるので実行してみましょう。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      int x = 10;

      Console.WriteLine(x);

      x100(x); // xの値は変わらない

      Console.WriteLine(x);
    }

    static void x100(int x)
    {
      x *= 100;
    }
  }
}

このように値型は関数を経由する時にコピーが行われるため、元の変数には影響が発生しないのです。

では、次は参照型の値渡しを理解しましょう。参照型の場合、コピーされるのは参照値です。

参照型の値渡し

つまり参照値をコピーした場合、関数内の参照値も同じ変数を示します。
これは引数で与える名前が違う場合でも、参照値が示す変数は同一です。

では、その動作を次のサンプルコードで確認してみましょう。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      int[] array = new int[] { 10, 20, 30 };

      foreach (int i in array)
        Console.WriteLine(i);

      x100(array); // 元の変数に影響する

      foreach (int i in array)
        Console.WriteLine(i);
    }

    static void x100(int[] array)
    {
      for (int i = 0; i < array.Length; i++)
        array[i] *= 100;
    }
  }
}

参照型である配列の要素が関数を経由することで変化しましたね。
このように値型と参照型では動作に大きなが違いがあります。

メリット・デメリット

当然にお互いにメリットとデメリットがあります。ただ、これを理解するにはスタックやヒープの概念を必要とするため、今回は大雑把に説明します。
まず、スタックやヒープとはメモリを確保する領域の種類です。そしてスタックでメモリを確保するほうが早いです。

じゃあ、全部スタックでいいじゃん。と思っても、そうはいかないのでヒープが存在します。
そしてヒープはメモリの動的確保ができます。動的確保とはプログラムの実行中に必要なサイズのメモリを確保することを言います。

C#に話を戻すと、参照型は動的確保を必要とするためヒープにしか確保できません。対して値型はスタックに確保されます。

管理人
管理人

ちなみにスタックと名前が付いてる通りでスタック構造をしてます。

では、メモリの確保に手間の掛かる参照型のメリットはなんでしょうか。それはデータのコピーを必要としないことです。

先程のサンプルコードを例に説明すると、配列の要素はint型3つで構成されてました。仮にこれが100個あったとしましょう。
この場合は4byte x 100 = 400byteのコピーが発生します。しかし、実際にコピーされたのは参照値です。
そして参照値は64bit環境なら8byteになります。このように無駄にコピーが発生しないので効率がいいです。

メリット・デメリット
管理人
管理人

このようにお互いにメリットとデメリットがあります。ちなみに変数へのアクセス速度って意味でもスタックのほうが早いです。

string型って参照型?

初心者を混乱に陥れる諸悪の根源です。C#のstring型は参照型になります。
では、ここで次のサンプルコードを実行してみましょう。


namespace Sample
{
  internal class Program
  {
    static void Main(string[] args)
    {
      string str = "りさ";

      Convert(str);

      Console.WriteLine(str);
    }

    static void Convert(string s)
    {
      s = "LISA";
    }
  }
}
りさ
りさ

本当に参照型? 関数内で変更したのに値が変わってないよ。

管理人
管理人

お気付きの通り参照型なのに値が変わりません。まるで値型です。

りさ
りさ

なんで?

管理人
管理人

そういう仕組みだから... 僕も設計者じゃないので細かいことは分かりません。
でも仕様としてそうなってます。気に入らない人は米Microsoftにでも電凸してください。

りさ
りさ

危険な行為を推奨するのやめてください。

管理人
管理人

冗談はさておき、これに関してはこういう仕様と理解してもらうしかないです。
文字列に関する処理で疑問を感じたら、これを思い出してください。

あとがき

今回の話は非常に重要です。これを知らないまま先に進むと、どこかで必ず詰みます。
全てを覚えろとは言いませんが、値型と参照型の値渡しの挙動の理解は必須です。
少し立ち止まってもいいので、必ず理解して先に進みましょう。

◆ C#に関する学習コンテンツ

この記事は参考になりましたか?

関連記事

コメント

この記事へのコメントはありません。