DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

値型と参照型の区別と違い
クラスと構造体の違い

.NET Frameworkの型(クラスや構造体など)は、「値型(Value type)」と「参照型(Reference Type)」に分類されます。両者の違いは分かりにくく、プログラミングを始めた方にとって障壁になっています。ここでは、「値型」と「参照型」を区別する方法と、違いについて説明します。

値型と参照型を区別する方法

とりあえずは、

構造体は値型、クラスは参照型

と覚えておいてください。

さらに細かく言えば、値型は、構造体、列挙体で、参照型は、クラス、配列、インターフェイス、デリゲートです。

具体的に言うと、Integer(C#では、int)やDouble、DateTimeなどは値型で、Formやコントロールなどは参照型です。

補足:VB.NETでは、全ての型は、値型か参照型のどちらかです。C#では、値型と参照型以外にポインタ型というのがあります。しかし、初心者はポインタ型を知らなくても全く問題ありません。

値型と参照型(クラスと構造体)の違い

入門書などを読むと、値型と参照型の違いは、「値型は実データそのものを格納しており、参照型は実データへの参照を格納している」などと説明されています。しかしこの説明をいきなりされても誰も理解できないでしょう。

この違いが如実に現れるのが、変数に代入した時です。ここからはこの点を中心に、両者の違いをできるだけ分かりやすく(分かりにくかったら申し訳ありませんが)説明したいと思います。

変数への代入における値型と参照型の違い

まずは、値型の変数に代入する例をご覧ください。

VB.NET
コードを隠すコードを選択
'Imports System.Drawing

' 1. Size構造体のインスタンスを作成し、val1に代入する。この時、val1のWidthは10。
Dim val1 As New Size(10, 10)
' 2. val1の値をval2に代入する。この時、val1とval2のWidthは10。
Dim val2 As Size = val1
' 3. val2のWidthの値を変更する。この時、val2のWidthは20。
val2.Width = 20

'結果を表示する
Console.WriteLine("val1:{0} val2:{1}", val1.Width, val2.Width)
'「val1:10 val2:20」と表示される
C#
コードを隠すコードを選択
//using System.Drawing;

// 1. Size構造体のインスタンスを作成し、val1に代入する。この時、val1のWidthは10。
Size val1 = new Size(10, 10);
// 2. val1の値をval2に代入する。この時、val1とval2のWidthは10。
Size val2 = val1;
// 3. val2のWidthの値を変更する。この時、val2のWidthは20。
val2.Width = 20;

//結果を表示する
Console.WriteLine("val1:{0} val2:{1}", val1.Width, val2.Width);
//「val1:10 val2:20」と表示される

この例では、値型(Size構造体)であるval1をval2に代入した後で、val2のプロパティの値を変更しています。そうすると、val1とval2のプロパティは違う値になります。この結果はごく当たり前で、不自然な点はありません。

これと全く同じことを参照型(Penクラス)で行ってみましょう。同じような結果になるでしょうか?

VB.NET
コードを隠すコードを選択
'Imports System.Drawing

' 1. Penクラスのインスタンスを作成し、ref1に代入する。この時、ref1のWidthは10。
Dim ref1 As New Pen(Color.Black, 10)
' 2. ref1をref2に代入する。この時、ref1とref2のWidthは10。
Dim ref2 As Pen = ref1
' 3. ref2のWidthの値を変更する。この時、ref2のWidthは20。
ref2.Width = 20

'結果を表示する
Console.WriteLine("ref1:{0} ref2:{1}", ref1.Width, ref2.Width)
'「ref1:20 ref2:20」と表示される

ref1.Dispose()
ref2.Dispose()
[VB.NET]
C#
コードを隠すコードを選択
//using System.Drawing;

// 1. Penクラスのインスタンスを作成し、ref1に代入する。この時、ref1のWidthは10。
Pen ref1 = new Pen(Color.Black, 10);
// 2. ref1をref2に代入する。この時、ref1とref2のWidthは10。
Pen ref2 = ref1;
// 3. ref2のWidthの値を変更する。この時、ref2のWidthは20。
ref2.Width = 20;

//結果を表示する
Console.WriteLine("ref1:{0} ref2:{1}", ref1.Width, ref2.Width);
//「ref1:20 ref2:20」と表示される

ref1.Dispose();
ref2.Dispose();

今度は先ほどとは結果が異なり、ref1とref2のWidthプロパティは同じ値になってしまいました。ref2のプロパティしか変更していないのに、なぜref1のプロパティまで変わってしまったのでしょうか?

値型の場合は、変数にデータ(インスタンス)そのものが入っています。val2にval1を代入すると、val1に入っているデータがコピーされて(複製されて)val2に入れられます。つまり、val1とval2のデータは、別のデータになります。よって、val2のプロパティを変更してもval1には何の影響も与えません。

参照型の場合は、変数に入っているのはデータそのものではなく、データがどこにあるかという情報(参照情報)です。ref2にref1を代入すると、ref1に入っている参照情報がコピーされてref2に入れられます。そうすると、ref1とref2に入っているのは同じ参照情報ですので、両者は同じデータを指している(参照している)ことになります。つまり、実はref1もref2も同じデータのため、ref2のプロパティを変更するとref1のプロパティも変わるというわけです。

上記の値型の代入と参照型の代入がどのように行われるかのイメージを図にしてみましたので、参考にしてください。

値型の代入

参照型の代入

参照型の代入におけるよくある勘違い

以上の説明で理解していただければ良いのですが、なかなか難しいものです。そこで、よくある間違いを紹介します。

例えば、先ほどのコードを1箇所だけ変えて、次のようにしてみます。変更する前と同じ結果になるでしょうか?

VB.NET
コードを隠すコードを選択
'Imports System.Drawing

Dim ref1 As New Pen(Color.Black, 10)
Dim ref2 As Pen = ref1
'次の箇所を変更する
'ref2.Width = 20
ref2 = New Pen(Color.Black, 20)

Console.WriteLine("ref1:{0} ref2:{1}", ref1.Width, ref2.Width)

ref1.Dispose()
ref2.Dispose()
C#
コードを隠すコードを選択
//using System.Drawing;

Pen ref1 = new Pen(Color.Black, 10);
Pen ref2 = ref1;
//次の箇所を変更する
//ref2.Width = 20;
ref2 = new Pen(Color.Black, 20);

Console.WriteLine("ref1:{0} ref2:{1}", ref1.Width, ref2.Width);

ref1.Dispose();
ref2.Dispose();

変更する前と同じように、ref1とref2が同じになると考えた方は、残念ながら間違いです。実際には、ref1のWidthが10、ref2のWidthが20になります。

なぜこのようになるのでしょうか?

変更後のコードは、新しいデータを作成して、その参照情報をref2に入れています。よって、ref2が参照するデータはこの新しいデータに変わります。ref1は相変わらずWidthが10のデータを参照したままですので、ref1とref2は別のデータを参照していることになります。

この時の状態を先ほどと同じような図に示すと、次のようになります(「参照型の代入」の図の3番目だけ変わりますので、そこだけ示します)。

参照型の変数が新しいデータを参照する時

結局変更後のコードでは、2行目の「ref2 = ref1」の部分は、全く意味のない、無駄なことを行なっていた訳です。

String型におけるよくある勘違い

上の繰り返しになりますが、念のためにString型についても補足しておきます。String型は参照型ですが、そのことを意識しすぎると、失敗してしまうことがあります。

まずは以下のコードをご覧ください。

VB.NET
コードを隠すコードを選択
Dim str1 As String = "こんにちは。"
Dim str2 As String = str1
str2 += "よろしくね。"

Console.WriteLine("str1:{0} str2:{1}", str1, str2)
'str1:こんにちは。 str2:こんにちは。よろしくね。
C#
コードを隠すコードを選択
string str1 = "こんにちは。";
string str2 = str1;
str2 += "よろしくね。";

Console.WriteLine("str1:{0} str2:{1}", str1, str2);
//str1:こんにちは。 str2:こんにちは。よろしくね。

String型が参照型だということを意識すると、このコードの結果は、str1とstr2の両方とも「こんにちは。よろしくね。」になりそうな気がしませんか?(しないという方は、これ以上お読みいただかなくても大丈夫です。)ところが実際にはそうはならず、str1が「こんにちは。」のままで、str2だけが「こんにちは。よろしくね。」になります。

3行目の、新しい文字列を追加している「+=」の部分は、現在str2が参照しているデータの末尾に文字列「よろしくね。」を付け足しているように見えるかもしれません。ところが実際はそうではなく、「こんにちは。」とは別の「こんにちは。よろしくね。」というデータを新しく作成しています。よってstr1とstr2は別のデータを参照しており、別の文字列になります。

実はStringクラスのプロパティやメソッドには、データの内容を変更できるものがありません。その代わりに、新しいデータが作成されます。このようにすることによって、String型が参照型だということを意識することなく使用できるようになっているのです。

補足:文字列のデータ内容を変更したい場合は、StringBuilderクラスを使用します。詳しくは、「文字列処理を高速に行う」をご覧ください。

メソッドのパラメータ

今までは変数への代入について説明してきました。しかし同じことがメソッドのパラメータについても当てはまります。

そのことが分かる例を以下に示します。

VB.NET
コードを隠すコードを選択
'Imports System.Windows.Forms
'Imports System.Drawing

'Button1のClickイベントハンドラ
Private Sub Button1_Click(ByVal sender As Object, _
                          ByVal e As EventArgs) Handles Button1.Click
    '値型と参照型のデータを作成する
    Dim val1 As New Size(10, 10)
    Dim ref1 As New Pen(Color.Black, 10)

    'ChangePropertyメソッドを呼び出す
    ChangeProperty(val1, ref1)

    '結果を表示する
    Console.WriteLine("val1:{0} ref1:{1}", val1.Width, ref1.Width)
    'val1:10 ref1:20

    ref1.Dispose()
End Sub

''' <summary>
''' パラメータのWidthプロパティを変更して20にする
''' </summary>
''' <param name="valArg">値型のパラメータ</param>
''' <param name="refArg">参照型のパラメータ</param>
Public Sub ChangeProperty(ByVal valArg As Size, ByVal refArg As Pen)
    valArg.Width = 20
    refArg.Width = 20
End Sub
C#
コードを隠すコードを選択
//using System.Windows.Forms;
//using System.Drawing;

//Button1のClickイベントハンドラ
private void Button1_Click(object sender, EventArgs e)
{
    //値型と参照型のデータを作成する
    Size val1 = new Size(10, 10);
    Pen ref1 = new Pen(Color.Black, 10);

    //ChangePropertyメソッドを呼び出す
    ChangeProperty(val1, ref1);

    //結果を表示する
    Console.WriteLine("val1:{0} ref1:{1}", val1.Width, ref1.Width);
    //val1:10 ref1:20

    ref1.Dispose();
}

/// <summary>
/// パラメータのWidthプロパティを変更して20にする
/// </summary>
/// <param name="valArg">値型のパラメータ</param>
/// <param name="refArg">参照型のパラメータ</param>
public void ChangeProperty(Size valArg, Pen refArg)
{
    valArg.Width = 20;
    refArg.Width = 20;
}

このコードのChangePropertyメソッドでは、パラメータのWidthプロパティを変更しています。メソッド内でプロパティを変更しているだけですので、外部には影響がないように思えます。確かに値型のパラメータではその通りですが、参照型のパラメータでは、引数に渡した変数のWidthプロパティが変わってしまいました。

この場合も、パラメータという変数に代入したと考えれば、今までと全く同じ考え方で理解できます。

補足:パラメータが値渡しでなく参照渡し(VB.NETではByRef、C#ではrefやout)の場合は、値型のパラメータでも、メソッド内でプロパティを変更すれば基の変数のプロパティも変わります。詳しくは、「値渡しと参照渡しの違いと使い分け」をご覧ください。

スタックとヒープ

初心者の方にとりあえず理解していただきたい事柄の説明は、以上で終わりです。これ以降は、余力のある方のみお読みください。

先の「変数への代入における値型と参照型の違い」の説明はやや抽象的でしたので、より具体的に説明します。

データはメモリに保存されています。値型のデータ(インスタント)や参照型の参照情報(つまり、変数が保持しているデータ)が保存されるのは、「スタック」と呼ばれるメモリ領域です。一方参照型のデータ(インスタント)が保存されるのは、「ヒープ」と呼ばれるメモリ領域です。

補足:ただし、参照型のフィールドとして宣言されている値型は、ヒープ上に格納されます。また、静的変数(共有変数)も、値型、参照型にかかわらず、ヒープ上に格納されます。

スタックにあるデータは、変数が使われなくなると(スコープの範囲外に出ると)、すぐに自動的に解放されます。一方ヒープにあるデータは、そのデータを参照する変数がなくなったと判断された後、ガベージコレクションによって、適当なタイミングで自動的に解放されます。

参照型の利点

値型と参照型の2つの型があるのは、非常に分かり難いです。しかも参照型は、データの位置を調べてからデータを探すという非効率的なことを行なっています。その上、参照型は解放が面倒です。すべての型を値型にすればよいのではないかと思ってしまいますが、なぜ参照型が必要なのでしょうか?

大きいデータを扱う場合は、参照型に多くの利点があります。例えば変数に代入する時、その度に大きなデータをコピーするのは大変ですが、参照情報のコピーだけならば簡単です。また、スタックに大きなデータを置くと、変数の検索に時間がかかってしまいます。

さらに参照型ならば、複数の場所から同じデータにアクセスすることができます。

ValueTypeクラス

値型は構造体で、参照型はクラスと説明しましたが、もっとシンプルに言うと、ValueTypeクラスから継承された型が値型で、それ以外が参照型です。

ValueTypeクラスはObjectクラスのEqualsメソッドをオーバーライドしていますので、値型と参照型でEqualsメソッドの動作が異なります。詳しくは、「自作クラスのEqualsメソッドをオーバーライドして、等価の定義を変更する」で説明しています。

その他の違い

上記以外の値型と参照型(クラスと構造体)の違いを列挙します。

  • 参照型はNothing(C#では、null)になることがあるが、値型はならない。ただし、値型でもnull許容型の場合は、Nothing(C#では、null)を代入することができる。
  • C#では、等値演算子(==)を値型で使用すると値の等価を調べるが、参照型で使用すると、String型などの一部の型を除いては、参照の等価を調べる。VB.NETでは、値の等価は等値演算子(=)を使って調べ、参照の等価はIs演算子を使って調べる。詳しくは、「2つの値が等しいか調べる」。
  • 値型をObject型に変換すると、ボックス化が実行される。
  • 値型からは新しい型を派生させることができない。
  • 値型には暗黙的に既定値を初期化する既定のコンストラクタが存在する。そのため、既定のコンストラクタを宣言することができない。また、インスタンスフィールドに変数初期化子を指定することができない。
  • 構造体はnew演算子を使わずにインスタンス化することができる。この場合コンストラクタが呼び出されないので高速だが、フィールドは未割り当てなので、初期化されるまで使用できない。
  • C#では、クラスと構造体でthisの意味が変わる。クラスではthisに代入することができないが、構造体ではthisに代入することができる。
  • 構造体はデストラクタを宣言できない。
  • 履歴:
  • 2012/8/29 説明を追加。

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • このサイトで紹介されているコードの多くは、例外処理が省略されています。例外処理については、こちらをご覧ください。
  • イベントハンドラの意味が分からない、C#のコードをそのまま書いても動かないという方は、こちらをご覧ください。
  • コードの先頭に記述されている「Imports ??? がソースファイルの一番上に書かれているものとする」(C#では、「using ???; がソースファイルの一番上に書かれているものとする」)の意味が分からないという方は、こちらをご覧ください。
  • 「???を参照に追加します」の意味が分からないという方は、こちらをご覧ください。
  • .NET Tipsをご利用いただく際は、注意事項をお守りください。