.NET Frameworkの型システムでは、型は値型(value type)と参照型(reference type)の二種類に大別されます。 具体的には、int, float, char, boolなどの基本型・構造体・列挙体などが値型となります。 また、オブジェクト型(object)・文字列型(string)・クラス・インターフェイスなどが参照型となります。

値型はデータに直接アクセスする型で、参照型は参照によってデータの実体にアクセスする型です。 C#やVB.NETなど.NET Frameworkの型システムをベースとする言語でも、値型と参照型の分類が存在します。

§1 値型と参照型

§1.1 値型と参照型の違い

値型と参照型の違いは値(インスタンス)へのアクセス方法にあります。 値型の変数は常になんらかの値(インスタンス)を格納していて、その値に対して直接アクセスします。 一方、参照型の変数はインスタンスへの参照を格納していて、参照を通して実体(インスタンス)にアクセスします。

また変数への代入時の動作も異なります。 値型の変数に対して代入を行う場合は代入元の値がコピーされて代入されるのに対して、参照型の変数に対して代入を行う場合は参照のみがコピーされて代入されます。 この時、インスタンス自体はコピーされません。

このため、値型では変数がそれぞれ別の値(インスタンス)を持つことになるため同一のインスタンスを参照することはありませんが、参照型では変数に格納される参照しだいでは同一の実体(インスタンス)を参照することもあります。

次のコードではこの違いを明確にしています。 ここで、構造体は値型、クラスは参照型であることを念頭に置いてください。

値型の代入と代入元への変更
using System;

// 構造体(値型)
struct ValType {
  public int ID;

  public ValType(int id)
  {
    ID = id;
  }
}

class Sample {
  static void Main()
  {
    ValType a = new ValType(1);

    // aをbに代入
    ValType b = a;
    // aの内容が複製されてbに代入されるため、
    // aとbは異なる実体を持つ

    Console.WriteLine("a.ID = {0}, b.ID = {1}",
                      a.ID, b.ID);

    // aに変更を加える
    a.ID = 2;
    // aとbは異なる実体であるため、aへの変更は
    // bには影響しない

    Console.WriteLine("a.ID = {0}, b.ID = {1}",
                      a.ID, b.ID);
  }
}
実行結果
a.ID = 1, b.ID = 1
a.ID = 2, b.ID = 1
参照型の代入と代入元への変更
using System;

// クラス(参照型)
class RefType {
  public int ID;

  public RefType(int id)
  {
    ID = id;
  }
}

class Sample {
  static void Main()
  {
    RefType a = new RefType(1);

    // aをbに代入
    RefType b = a;
    // (aのインスタンスへの参照がbに代入されるため、
    // aとbは同一の実体を参照する)

    Console.WriteLine("a.ID = {0}, b.ID = {1}",
                      a.ID, b.ID);

    // aに変更を加える
    a.ID = 2;
    // aとbは同一の実体を参照するため、aの実体への
    // 変更はbの実体に変更を加えるのと同じ事となる

    Console.WriteLine("a.ID = {0}, b.ID = {1}",
                      a.ID, b.ID);
  }
}
実行結果
a.ID = 1, b.ID = 1
a.ID = 2, b.ID = 2

このように、代入を行った後に代入元(変数a)に変更を加えた場合、値型の場合では代入先(変数b)に影響しないのに対して、参照型の場合では見かけ上代入元への変更は代入先にも影響するような動作となります。

この他にも値型と参照型でいくつかの相違点があり、まとめると次のようになります。

値型と参照型の相違点
値型 参照型
変数に代入される値 インスタンス(値)そのもの インスタンスへの参照
代入時の動作 値の複製(コピー)が代入される 参照が代入される
ヌル参照 変数をヌル参照にすることはできない 変数をヌル参照にすることができる
デフォルトコンストラクタ 暗黙的に実装される
明示的に実装することはできない
必要なら明示的に実装することができる
インスタンスがアロケートされる場所 スタック ヒープ
インスタンスが破棄されるタイミング スコープから脱した時点で破棄される ガベージコレクタにより定期的に破棄される

§1.2 値型と参照型の分類

.NET Frameworkの型システムにおいては、値型と参照型のどちらに分類されるかは明確に定められています。 値型・参照型となる型はそれぞれ次のようになります。

  • 値型
    • プリミティブ型
      • 数値型 (int, float, IntPtr等)
      • 文字型 (char)
      • ブール型 (bool)
    • 構造体
    • 列挙体
  • 参照型
    • クラス
    • インターフェイス
    • デリゲート
    • オブジェクト型 (object)
    • 文字列型 (string)
    • 配列

.NET Frameworkの型システムにおいてはint(System.Int32)やfloat(System.Single)などのプリミティブ型も構造体であることから、おおまかに「構造体・列挙体が値型、それ以外が参照型」と分類することができます。 Type.IsValueTypeプロパティを参照することで実行時に型が値型かどうかを知ることができます。



§2 値型・参照型の挙動の違い

以下では値型・参照型の挙動の違いや扱う上での注意点、構造体とクラスの使い分けなどについて解説します。

§2.1 値渡し・参照渡し

メソッドの引数を値渡しする場合、値型の場合は代入の際と同様インスタンスの複製がメソッドに渡されるため、メソッド内で引数に変更を加えても呼び出し元の変数には反映されません。

一方参照型の場合、メソッドの引数には参照が渡されるため、呼び出し元の変数と同一のインスタンスを参照します。 そのため、メソッド内で引数に変更を加えると呼び出し元の変数に反映されます。

値型の値渡しと引数への変更
using System;

// 構造体(値型)
struct ValType {
  public int ID;

  public ValType(int id)
  {
    ID = id;
  }
}

class Sample {
  static void Method(ValType v)
  {
    // 引数で渡される値に変更を加える
    v.ID = 2;
  }

  static void Main()
  {
    ValType v = new ValType(1);

    Console.WriteLine("v.ID = {0}", v.ID);

    Method(v);

    Console.WriteLine("v.ID = {0}", v.ID);
  }
}
実行結果
v.ID = 1
v.ID = 1
参照型の値渡しと引数への変更
using System;

// クラス(参照型)
class RefType {
  public int ID;

  public RefType(int id)
  {
    ID = id;
  }
}

class Sample {
  static void Method(RefType r)
  {
    // 引数で渡されるインスタンスに変更を加える
    r.ID = 2;
  }

  static void Main()
  {
    RefType r = new RefType(1);

    Console.WriteLine("r.ID = {0}", r.ID);

    Method(r);

    Console.WriteLine("r.ID = {0}", r.ID);
  }
}
実行結果
r.ID = 1
r.ID = 2

メソッドの引数を参照渡し(ref/ByRef)する場合は、値型の場合でも引数に指定したインスタンスを参照することになるため、メソッド内で引数に変更を加えると参照型の場合と同様に呼び出し元の変数に反映されます。

値型の参照渡しと引数への変更
using System;

// 構造体(値型)
struct ValType {
  public int ID;

  public ValType(int id)
  {
    ID = id;
  }
}

class Sample {
  static void Method(ref ValType v)
  {
    // 引数で渡される値に変更を加える
    v.ID = 2;
  }

  static void Main()
  {
    ValType v = new ValType(1);

    Console.WriteLine("v.ID = {0}", v.ID);

    Method(ref v);

    Console.WriteLine("v.ID = {0}", v.ID);
  }
}
実行結果
v.ID = 1
v.ID = 2
参照型の参照渡しと引数への変更
using System;

// クラス(参照型)
class RefType {
  public int ID;

  public RefType(int id)
  {
    ID = id;
  }
}

class Sample {
  static void Method(ref RefType r)
  {
    // 引数で渡されるインスタンスに変更を加える
    r.ID = 2;
  }

  static void Main()
  {
    RefType r = new RefType(1);

    Console.WriteLine("r.ID = {0}", r.ID);

    Method(ref r);

    Console.WriteLine("r.ID = {0}", r.ID);
  }
}
実行結果
r.ID = 1
r.ID = 2

§2.2 値型のプロパティ・インデクサ

値型では代入時にコピーが作成されますが、値型のプロパティやインデクサから値を取得しようとする場合も同様にコピーが作成されます。 値型のプロパティ・インデクサはインスタンスそのものではなくインスタンスのコピーを返すことから、直接インスタンスを変更することができません。

そのため、次の例のように値型のプロパティを直接変更しようとするとコンパイルエラーとなります。 参照型では変更しようとするインスタンスを参照によって取得することができるため、コンパイルエラーとはなりません。

値型のプロパティに対する変更
using System;

// 構造体(値型)
struct ValType {
  public int ID;
}

class C {
  ValType v = new ValType();

  // 値型のプロパティ
  public ValType V {
    get { return v; }
    set { v = value; }
  }

  public override string ToString()
  {
    return string.Format("v.ID = {0}", v.ID);
  }
}

class Sample {
  static void Main()
  {
    C c = new C();

    Console.WriteLine(c);

    // 値型のプロパティに変更を加えようとする
    c.V.ID = 3;
    // error CS1612: 変数ではないため、
    // 'C.V' の戻り値を変更できません。

    Console.WriteLine(c);
  }
}
実行結果
(コンパイルエラーとなるため実行できない)
参照型のプロパティに対する変更
using System;

// クラス(参照型)
class RefType {
  public int ID;
}

class C {
  RefType r = new RefType();

  // 参照型のプロパティ
  public RefType R {
    get { return r; }
    /* setterは必要ない */
  }

  public override string ToString()
  {
    return string.Format("r.ID = {0}", r.ID);
  }
}

class Sample {
  static void Main()
  {
    C c = new C();

    Console.WriteLine(c);

    // 参照型のプロパティに変更を加える
    c.R.ID = 3;
    // (これはコンパイルエラーとはならない)


    Console.WriteLine(c);
  }
}
実行結果
r.ID = 0
r.ID = 3

コンパイルエラーとならないようにするには、次の例のように一旦プロパティの値を一時変数に代入してコピーし、変更を加えた後にプロパティに再代入します。

一時変数を使って値型のプロパティを変更する
class Sample {
  static void Main()
  {
    C c = new C();

    Console.WriteLine(c);

    // 現在のプロパティの値を一時変数にコピーする
    ValType v = c.V;

    // 一時変数に代入したインスタンスに対して変更を加える
    v.ID = 3;

    // 変更した一時変数のインスタンスをプロパティにコピーする
    c.V = v;

    Console.WriteLine(c);
  }
}
実行結果
v.ID = 0
v.ID = 3

値型配列の要素を直接変更することはできますが、値型のインデクサの場合もプロパティと同様に直接変更することはできません。

値型のインデクサに対する変更
using System;
using System.Collections.Generic;

// 構造体(値型)
struct ValType {
  public int ID;
}

class Sample {
  static void Main()
  {
    ValType[] arr = new ValType[1];

    // 配列の要素を変更
    arr[0].ID = 3;

    List<ValType> list = new List<ValType>(new ValType[1]);

    // インデクサを使用して要素を変更
    list[0].ID = 3;
    // error CS1612: 変数ではないため、'System.Collections.Generic.List<ValType>.this[int]'の戻り値を変更できません。
  }
}

さらに、インスタンスに変更を加えるようなメソッドを呼び出す場合も同様の問題が発生します。 以下の例ではプロパティで取得した値型インスタンスのメソッドを呼び出していますが、メソッド呼び出しで変更されるのはあくまで取得によってコピーされたインスタンスであって、元のインスタンスには一切影響しないため、一見するとメソッド呼び出しによる変更が反映されないような動作となります。

値型のプロパティとメソッド呼び出し
using System;

// 構造体(値型)
struct ValType {
  public int ID;

  // IDフィールドの値を設定するメソッド
  public void SetID(int newID)
  {
    ID = newID;
  }
}

class C {
  ValType v = new ValType();

  // 値型のプロパティ
  public ValType V {
    get { return v; }
  }

  public override string ToString()
  {
    return string.Format("v.ID = {0}", v.ID);
  }
}

class Sample {
  static void Main()
  {
    C c = new C();

    Console.WriteLine(c);

    // 値型のプロパティに変更を加えようとする
    c.V.SetID(3);

    // 意図に反して変更が反映されないように見える
    Console.WriteLine(c);
  }
}
実行結果
v.ID = 0
v.ID = 0
参照型のプロパティとメソッド呼び出し
using System;

// クラス(参照型)
class RefType {
  public int ID;

  // IDフィールドの値を設定するメソッド
  public void SetID(int newID)
  {
    ID = newID;
  }
}

class C {
  RefType r = new RefType();

  // 参照型のプロパティ
  public RefType R {
    get { return r; }
  }

  public override string ToString()
  {
    return string.Format("r.ID = {0}", r.ID);
  }
}

class Sample {
  static void Main()
  {
    C c = new C();

    Console.WriteLine(c);

    // 参照型のプロパティに変更を加える
    c.R.SetID(3);

    // 意図したとおり変更が反映される
    Console.WriteLine(c);
  }
}
実行結果
r.ID = 0
r.ID = 3

この場合も、先の例と同様に一時変数に代入してからメソッド呼び出しを行うことで変更を反映させることができます。

一見すると予想に反する動作であるにも関わらず、このようなコードはコンパイルエラーとはならないため注意する必要があります。 インデクサを多用するListやDictionaryなどのジェネリックコレクションで値型を扱う場合にはこういった問題に遭遇しやすいので注意が必要です。

§2.3 同値性・同一性の比較

Equalsメソッドを使うと、二つのインスタンスが等しいかどうかの比較が行われます。 Equalsメソッドのデフォルトの動作は値型と参照型で次のように異なります。

値型の場合
二つの値がビット単位で等しい場合にtrueとなる
参照型の場合
二つの参照が同一のインスタンスを参照している場合にtrueとなる

つまり、値型では同値性の比較が行われ、参照型では同一性の比較が行われます。

Equalsによる値型の比較(同値性の比較)
using System;

// 構造体(値型)
struct ValType {
  public int ID;

  public ValType(int id)
  {
    ID = id;
  }
}

class Sample {
  static void Main()
  {
    ValType a = new ValType(1);
    ValType b = a;
    ValType c = new ValType(2);

    b.ID = 2;

    // Equalsメソッドで3つの変数を比較する
    Console.WriteLine("a.Equals(b) : {0}", a.Equals(b));
    Console.WriteLine("a.Equals(c) : {0}", a.Equals(c));
    Console.WriteLine("b.Equals(c) : {0}", b.Equals(c));
  }
}
実行結果
a.Equals(b) : False
a.Equals(c) : False
b.Equals(c) : True
Equalsによる参照型の比較(同一性の比較)
using System;

// クラス(参照型)
class RefType {
  public int ID;

  public RefType(int id)
  {
    ID = id;
  }
}

class Sample {
  static void Main()
  {
    RefType a = new RefType(1);
    RefType b = a;
    RefType c = new RefType(2);

    b.ID = 2;

    // Equalsメソッドで3つの変数を比較する
    Console.WriteLine("a.Equals(b) : {0}", a.Equals(b));
    Console.WriteLine("a.Equals(c) : {0}", a.Equals(c));
    Console.WriteLine("b.Equals(c) : {0}", b.Equals(c));
  }
}
実行結果
a.Equals(b) : True
a.Equals(c) : False
b.Equals(c) : False

EqualsメソッドをオーバーライドしたりIEquatable<T>インターフェイスを実装することでEqualsメソッドの動作を変えることができます。 例えば、String.Equalsメソッドが文字列の同値性の比較を行うように、参照型でも同値性の比較を行うように実装することができます。 このほか、任意の型同士で参照の比較(同一性の比較)を行いたい場合は、Object.ReferenceEqualsメソッドを使うことができます。

§2.4 ボックス化

値型のインスタンスをobject型変数に代入する場合、スタックに配置されている値型インスタンスの複製が作成され、object型変数に箱詰めした上でヒープに配置されます。 これをボックス化(boxing)と呼びます。 ボックス化の際インスタンスの複製が作成されるため、元のインスタンスとボックス化されたインスタンスは別々のものとなります。 そのため、元のインスタンスに変更を加えてもボックス化されたインスタンスには影響しません。

参照型のインスタンスをobject型変数に代入する場合は単にアップキャストとなるだけで、ボックス化は行われません。 参照がobject型変数に代入されるだけとなるため、当然object型に代入されるインスタンスは元のインスタンスと同一のものとなります。

値型インスタンスのボックス化
using System;

// 構造体(値型)
struct ValType {
  public int ID;

  public ValType(int id)
  {
    ID = id;
  }

  public override string ToString()
  {
    return string.Format("ID = {0}", ID);
  }
}

class Sample {
  static void Main()
  {
    ValType v = new ValType(1);
    object o = v; // ボックス化

    Console.WriteLine("v:{0}, o:{1}", v, o);

    v.ID = 2;

    Console.WriteLine("v:{0}, o:{1}", v, o);
  }
}
実行結果
v:ID = 1, o:ID = 1
v:ID = 2, o:ID = 1
参照型インスタンスのアップキャスト
using System;

// クラス(参照型)
class RefType {
  public int ID;

  public RefType(int id)
  {
    ID = id;
  }

  public override string ToString()
  {
    return string.Format("ID = {0}", ID);
  }
}

class Sample {
  static void Main()
  {
    RefType r = new RefType(1);
    object o = r; // アップキャスト

    Console.WriteLine("r:{0}, o:{1}", r, o);

    r.ID = 2;

    Console.WriteLine("r:{0}, o:{1}", r, o);
  }
}
実行結果
r:ID = 1, o:ID = 1
r:ID = 2, o:ID = 2

逆に、object型変数から値型のインスタンスを取り出す際にはボックス化解除(unboxing)が行われます。 ボックス化解除では、object型に箱詰めされている値型インスタンスを取り出して値型変数に代入します。

object型変数への代入だけでなく、値型が実装するインターフェイス型へ代入する場合にもボックス化が行われます。

ボックス化ではインスタンスの複製が作成されるため、参照型のキャストと比べると若干コストのある操作となります。 値型のボックス化・ボックス化解除と参照型のアップキャスト・ダウンキャストの速度を比較すると次のようになります。

値型のボックス化・ボックス化解除の速度
using System;
using System.Diagnostics;

// 構造体(値型)
struct ValType {}

class Sample {
  static void Main()
  {
    for (var c = 0; c < 5; c++) {
      var v = new ValType();
      object o;

      var sw = Stopwatch.StartNew();

      for (var i = 0; i < 10 * 1000 * 1000; i++) {
        o = v;          // ボックス化
        v = (ValType)o; // ボックス化解除
      }

      Console.WriteLine(sw.Elapsed);
    }
  }
}
.NET Framework 4.0での実行結果
00:00:00.0153980
00:00:00.0149546
00:00:00.0161484
00:00:00.0174094
00:00:00.0154963
Mono 2.10.9での実行結果
00:00:00.0384817
00:00:00.0356860
00:00:00.0328524
00:00:00.0318040
00:00:00.0316627
参照型のアップキャスト・ダウンキャストの速度
using System;
using System.Diagnostics;

// クラス(参照型)
class RefType {}

class Sample {
  static void Main()
  {
    for (var c = 0; c < 5; c++) {
      var r = new RefType();
      object o;

      var sw = Stopwatch.StartNew();

      for (var i = 0; i < 10 * 1000 * 1000; i++) {
        o = r;          // アップキャスト
        r = (RefType)o; // ダウンキャスト
      }

      Console.WriteLine(sw.Elapsed);
    }
  }
}
.NET Framework 4.0での実行結果
00:00:00.0012479
00:00:00.0012820
00:00:00.0012420
00:00:00.0012590
00:00:00.0012509
Mono 2.10.9での実行結果
00:00:00.0027001
00:00:00.0026039
00:00:00.0025005
00:00:00.0025088
00:00:00.0026163

非ジェネリックコレクションで値型を扱う場合、コレクションへの格納・取り出しを行う度にボックス化・ボックス化解除されることになります。 そのため、パフォーマンスの観点からもArrayListなどの非ジェネリックコレクションよりもボックス化・ボックス化解除が発生しないListなどのジェネリックコレクションを使うことが推奨されます。

§2.5 代入の速度

値型の代入ではインスタンスの複製が行われるため、型のサイズが大きくなるほど代入にかかるコストは大きくなります。 参照型ではインスタンスの複製は行われないため、型のサイズによらず代入にかかるコストは一定となります。

計4×1バイトのフィールドを持つ値型の代入
using System;
using System.Diagnostics;

// 構造体(値型)
struct ValType {
  public int Field1;
}

class Sample {
  static void Main()
  {
    for (var c = 0; c < 5; c++) {
      ValType v1 = new ValType();
      ValType v2;

      var sw = Stopwatch.StartNew();

      for (var i = 0; i < 100 * 1000 * 1000; i++) {
        v2 = v1; // 代入
      }

      Console.WriteLine(sw.Elapsed);
    }
  }
}
.NET Framework 4.0での実行結果
00:00:00.1371791
00:00:00.1295024
00:00:00.1292611
00:00:00.1309864
00:00:00.0770210
Mono 2.10.9での実行結果
00:00:00.0967827
00:00:00.0875368
00:00:00.0853119
00:00:00.0854036
00:00:00.0864029
計4×1バイトのフィールドを持つ参照型の代入
using System;
using System.Diagnostics;

// クラス(参照型)
class RefType {
  public int Field1;
}

class Sample {
  static void Main()
  {
    for (var c = 0; c < 5; c++) {
      RefType r1 = new RefType();
      RefType r2;

      var sw = Stopwatch.StartNew();

      for (var i = 0; i < 100 * 1000 * 1000; i++) {
        r2 = r1; // 代入
      }

      Console.WriteLine(sw.Elapsed);
    }
  }
}
.NET Framework 4.0での実行結果
00:00:00.1231474
00:00:00.1206678
00:00:00.0887434
00:00:00.0872789
00:00:00.0861327
Mono 2.10.9での実行結果
00:00:00.1159542
00:00:00.0857808
00:00:00.0854572
00:00:00.0859070
00:00:00.0863372
計4×8バイトのフィールドを持つ値型の代入
using System;
using System.Diagnostics;

// 構造体(値型)
struct ValType {
  public int Field1;
  public int Field2;
  public int Field3;
  public int Field4;
  public int Field5;
  public int Field6;
  public int Field7;
  public int Field8;
}

class Sample {
  static void Main()
  {
    for (var c = 0; c < 5; c++) {
      ValType v1 = new ValType();
      ValType v2;

      var sw = Stopwatch.StartNew();

      for (var i = 0; i < 100 * 1000 * 1000; i++) {
        v2 = v1; // 代入
      }

      Console.WriteLine(sw.Elapsed);
    }
  }
}
.NET Framework 4.0での実行結果
00:00:00.1145360
00:00:00.1139195
00:00:00.0907852
00:00:00.0858125
00:00:00.0871278
Mono 2.10.9での実行結果
00:00:00.2255986
00:00:00.1306481
00:00:00.1284407
00:00:00.1298623
00:00:00.1288758
計4×8バイトのフィールドを持つ参照型の代入
using System;
using System.Diagnostics;

// クラス(参照型)
class RefType {
  public int Field1;
  public int Field2;
  public int Field3;
  public int Field4;
  public int Field5;
  public int Field6;
  public int Field7;
  public int Field8;
}

class Sample {
  static void Main()
  {
    for (var c = 0; c < 5; c++) {
      RefType r1 = new RefType();
      RefType r2;

      var sw = Stopwatch.StartNew();

      for (var i = 0; i < 100 * 1000 * 1000; i++) {
        r2 = r1; // 代入
      }

      Console.WriteLine(sw.Elapsed);
    }
  }
}
.NET Framework 4.0での実行結果
00:00:00.1113680
00:00:00.1196529
00:00:00.0888160
00:00:00.0872582
00:00:00.0874921
Mono 2.10.9での実行結果
00:00:00.1106691
00:00:00.0856765
00:00:00.0905668
00:00:00.0860375
00:00:00.0907923

サイズの大きい構造体の代入を多数行う必要がある場合、クラスに置き換えることを検討することでコストを下げることができます。 クラスまたは構造体の選択(MSDN)では、構造体とクラスのどちらを選択するかという基準の1つに「サイズが16バイト未満かどうか」というガイドラインが設定されています。

§2.6 インスタンスの複製 (Object.MemberwiseClone)

Object.MemberwiseCloneメソッドを使ってインスタンスの複製を作成する際、フィールドが値型か参照型かによって複製時の動作が異なります。

値型のフィールドはビット単位での複製(詳細コピー)が行われるのに対し、参照型のフィールドは参照のみが複製されます(簡易コピー)。 そのため、MemberwiseCloneメソッドによるインスタンス複製後の参照型フィールドは、複製元の同一フィールドと同じインスタンスを参照することになります。

MemberwiseCloneを使ってインスタンスを複製する例
using System;

// 構造体(値型)
struct ValType {
  public int ID;
}

// クラス(参照型)
class RefType {
  public int ID;
}

class C {
  // 値型フィールド
  public ValType V = new ValType();
  // 参照型フィールド
  public RefType R = new RefType();

  // インスタンスの複製を作成するメソッド
  public C Clone()
  {
    return (C)MemberwiseClone();
  }
}

class Sample {
  static void Main()
  {
    C c1 = new C();

    Console.WriteLine("c1.V.ID = {0}, c1.R.ID = {1}", c1.V.ID, c1.R.ID);

    // インスタンスを複製する
    C c2 = c1.Clone();

    // 複製後のインスタンスのフィールドに変更を加える
    c2.V.ID = 2;
    c2.R.ID = 2;

    Console.WriteLine("c1.V.ID = {0}, c1.R.ID = {1}", c1.V.ID, c1.R.ID);
    Console.WriteLine("c2.V.ID = {0}, c2.R.ID = {1}", c2.V.ID, c2.R.ID);

    Console.WriteLine("Object.ReferenceEquals(c1.R, c2.R) = {0}", Object.ReferenceEquals(c1.R, c2.R));
  }
}
実行結果
c1.V.ID = 0, c1.R.ID = 0
c1.V.ID = 0, c1.R.ID = 2
c2.V.ID = 2, c2.R.ID = 2
Object.ReferenceEquals(c1.R, c2.R) = True

インスタンスの複製とMemberwiseCloneメソッド、詳細コピーと簡易コピーの動作についてはオブジェクトの複製 §.Object.MemberwiseCloneによる複製でも解説しています。

§2.7 デフォルト値

初期値を指定しないでフィールドやローカル変数の宣言を行った場合、デフォルト値で初期化されます。 値型のデフォルト値は0(もしくは0に相当する値)で、参照型のデフォルト値はヌル参照(null/Nothing)です。

型のデフォルト値について詳しくは型の種類・サイズ・精度・値域 §.型のデフォルト値を参照してください。

§3 値型か参照型か調べる

実行時に型が値型か参照型かを調べるには、型情報(Type)を取得しIsValueTypeプロパティを参照します。

型情報を取得して値型か参照型か調べる
using System;

// 構造体(値型)
struct ValType {}

// クラス(参照型)
class RefType {}

class Sample {
  static void PrintType(Type t)
  {
    Console.WriteLine("{0,-20}: {1}", t.FullName, t.IsValueType ? "値型" : "参照型");
  }

  static void Main()
  {
    PrintType(typeof(int));
    PrintType(typeof(string));
    PrintType(typeof(object));
    PrintType(typeof(ValType)); // 構造体
    PrintType(typeof(RefType)); // クラス
    PrintType(typeof(int[])); // 配列
    PrintType(typeof(DayOfWeek)); // 列挙型
    PrintType(typeof(IDisposable)); // インターフェイス型
    PrintType(typeof(EventHandler)); // デリゲート型
  }
}
実行結果
System.Int32        : 値型
System.String       : 参照型
System.Object       : 参照型
ValType             : 値型
RefType             : 参照型
System.Int32[]      : 参照型
System.DayOfWeek    : 値型
System.IDisposable  : 参照型
System.EventHandler : 参照型

型情報の取得について、およびTypeクラスについて詳しくはリフレクションで解説しています。

§4 ジェネリック型の制約

ジェネリック型を定義する際、型パラメータに制約(constraints)を持たせることができます。 型パラメータに指定できる型を特定の基本型やインターフェイスを実装する型に限定するのと同様に、値型・参照型のみに限定させることもできます。

C#ではwhere句を使ってwhere T : structとすれば型パラメータTを値型に、where T : classとすれば参照型に限定することができます。 VBではOf句を使ってOf T As StructureとすればTを値型に、Of T As Classとすれば参照型に限定できます。

型パラメータに値型・参照型の制約を加える例
using System;
using System.Collections.ObjectModel;

// 型パラメータTを値型のみに限定したコレクション
class ValTypeCollection<T> : Collection<T> where T : struct {
}

// 型パラメータTを参照型のみに限定したコレクション
class RefTypeCollection<T> : Collection<T> where T : class {
}

class Sample {
  static void Main()
  {
    // intは値型なので型を構築できる
    ValTypeCollection<int> intcol;

    // stringは参照型なので制約と一致せず、型を構築できない
    // (コンパイルエラーとなる)
    ValTypeCollection<string> strcol;
    // error CS0453: 型 'string' は、ジェネリック型のパラメーター 'T'、またはメソッド 'ValTypeCollection<T>' として使用するために、Null非許容の値型でなければなりません

    // floatは値型なので制約と一致せず、型を構築できない
    // (コンパイルエラーとなる)
    RefTypeCollection<float> fltcol;
    // error CS0452: 型 'float' は、ジェネリック型のパラメーター 'T'、またはメソッド 'RefTypeCollection<T>' として使用するために、参照型でなければなりません
  }
}