C#の値型と参照型の違い

C#

C#の変数には値型参照型との2つのタイプが存在します。
(ポインタ型というのも存在しますがよほどのことが無い限り使いませんし使うべきでないと思ってます)

「値型」は構造体(struct)列挙型(enum) です。
int や double といった数値型は実は構造体で定義されているので値型です。

※詳しくは「C#のStringとstring、Int32とint 違いは・・・ない!」をご覧ください

「参照型」はクラス(class)配列です。
インターフェース(interface)デリゲート(delegate) も参照型になります。


値型の変数は値そのものを格納するのに対して、参照型の変数は実体がある場所(アドレス)を格納するという違いがあるのですが、この2つの違いを正しく理解する事はとても重要です。


変数がメモリ上でどのように管理されるか

値型である構造体と、参照型であるクラスで似たようなコードを作ってみました。
それぞれの変数がメモリ上でどのように管理されているかを見てみましょう。

値型(構造体) 参照型(クラス)
    public struct Point
    {
        public int x;
        public int y;
    }


    public static void Main(string[] args)
    {
        Point p1;
        Point p2;


        p1.x = 1;
        p1.y = 2;
        p2 = p1;
    }
10~11行目で2つの変数が作られました。
以下はメモリ空間のイメージです。
値型メモリ空間
値型の変数は値そのものを格納するので p1.x・p1.y・p2.x・p2.y を格納する領域が スタックメモリという場所に割り当てられます。
    public class Point
    {
        public int x;
        public int y;
    }


    public static void Main(string[] args)
    {
        Point p1;
        Point p2;

        p1 = new Point();
        p1.x = 1;
        p1.y = 2;
        p2 = p1;
    }
10~11行目で2つの変数が作られました。
以下はメモリ空間のイメージです。
参照型メモリ空間
参照型は実体の参照先アドレスを格納するので、 p1とp2という2つのアドレス格納用の領域が スタックメモリという場所に割り当てられます。 但し、この時点ではまだ実体がありません。
13行目で new をしています。
実体を作る作業(インスタンス生成)です。
ここで Point の x・y を格納する領域が ヒープメモリという場所に割り当てられ、 そのアドレスが変数 p1 の値に記録されます。
参照型メモリ空間

16行目で代入が行われています。
値型メモリ空間
p1 の x・y の値がそれぞれ p2 にコピーされます。
16行目で代入が行われています。
参照型メモリ空間
参照型はアドレスがコピーされるだけです。
p1 も p2 も同じ x・y を参照する事になります。




代入の違いに注意

値型と参照型がどのようにメモリ上で管理されているかが分かれば、代入した時の動きの違いが分かるはずです。

値型(構造体) 参照型(クラス)
    public struct Point
    {
        public int x;
        public int y;
    }
 
 
    public static void Main(string[] args)
    {
        Point p1;
        Point p2;
 
 
        p1.x = 1;
        p1.y = 2;
        p2 = p1;
 
        p2.x = 5;
        Console.WriteLine("p1.x=" + p1.x + "  p1.y=" + p1.y);
        Console.WriteLine("p2.x=" + p2.x + "  p2.y=" + p2.y);
    }
p1 を p2 に代入した後、p2.x の値を書き換えています。
値型は p1 を p2 それぞれが x・y の格納場所を持っているので結果は以下のように p1 と p2 は異なります。
    public class Point
    {
        public int x;
        public int y;
    }
 
 
    public static void Main(string[] args)
    {
        Point p1;
        Point p2;
 
        p1 = new Point();
        p1.x = 1;
        p1.y = 2;
        p2 = p1;
 
        p2.x = 5;
        Console.WriteLine("p1.x=" + p1.x + "  p1.y=" + p1.y);
        Console.WriteLine("p2.x=" + p2.x + "  p2.y=" + p2.y);
    }
p1 を p2 に代入した後、p2.x の値を書き換えています。
18行目の代入は本体の x を値を書き換えます。
p1 と p2 は同じアドレスを参照しているので結果は以下のように同じものになります。
p1.x=1  p1.y=2
p2.x=5  p2.y=2
p1.x=5  p1.y=2
p2.x=5  p2.y=2



メソッドの引数に注意

メソッドの引数に渡す場合にも、上記の代入と同じような事が起こります。

値型(構造体) 参照型(クラス)
    public struct Point
    {
        public int x;
        public int y;
    }
 
 
    public static void TestProc(Point prm)
    {
        prm.x = 8;
    }
 
 
    public static void Main(string[] args)
    {
        Point p;
 
        
        p.x = 1;
        p.y = 2;
        TestProc(p);
        Console.WriteLine("p.x=" + p.x + "  p.y=" + p.y);
    }
11行目でTestProc メソッドへ p が渡されます。
この時 prm という変数がメモリ上に確保され p の内容(xとy)が prm にコピーされるという事が行われます。

コピーされた prm.x の値が変更されたとしても、 コピー元の p.x に変化はありません。
    public class Point
    {
        public int x;
        public int y;
    }
 
 
    public static void TestProc(Point prm)
    {
        prm.x = 8;
    }
 
 
    public static void Main(string[] args)
    {
        Point p;
 
        p = new Point();
        p.x = 1;
        p.y = 2;
        TestProc(p);
        Console.WriteLine("p.x=" + p.x + "  p.y=" + p.y);
    }
11行目でTestProc メソッドへ p が渡されます。
この時 prm という変数がメモリ上に確保され p の内容(アドレス)が prm がコピーされるという事が行われます。

prm も p と同じ本体を参照しているので、 prm.x を変更すれば p.x も変わります。
p.x=1  p.y=2
p.x=8  p1.y=2



それぞれのメリット・デメリット

変数へ値を格納するという部分だけ見ると、参照型では「変数の作成」→「インスタンスの作成」という2段階のステップが必要となり処理コストがわずかですが大きい事が分かります。

しかしメソッドの引数に渡す場合などは、新たに変数が作られ値がコピーされて利用されます。
仮に構造体が数キロバイトもある巨大なものだったとしたら、そのすべてをコピーする作業は大変な処理コストになると予想できます。
(参照型であればアドレスを格納する為のわずかなサイズの変数をコピーするだけで済みます)

int や double のような小さなサイズのデータであれば値型の方が都合がよく、
文字列や配列のような大きなサイズのデータでれば参照型の方が都合がよいというわけです。



コメント