C# の値型と参照型の詳細解説

C# の値型と参照型の完全解説

一、根本的な違い(メモリ上の格納)

1. メモリの割り当て位置

  • 値型(Value Type) :変数のデータはスタック(Stack)に直接格納され、変数自体がデータそのものです。
  • 参照型(Reference Type) :実際のデータはヒープ(Heap)に格納され、スタック上にはメモリアドレス(参照)のみが格納され、そのアドレスを通じてヒープ内のオブジェクトを指し示します。

2. 代入動作の違い

  1. 値型の代入:完全なコピーの作成
    代入時にデータ全体をコピーするため、2つの変数は完全に独立しており、一方を変更しても他方には影響しません。
  2. 参照型の代入:アドレスのコピー(浅いコピー)
    ヒープのアドレスのみをコピーし、2つの変数は同じヒープ上のオブジェクトを指します。いずれか一方のオブジェクトの内容を変更すると、両方が同時に変化します。

3. ライフサイクルとガベージコレクション

  • 値型:スタック上で自動的に解放され、スコープ外になると直ちに破棄されるため、GCのオーバーヘッドは発生しない。
  • 参照型:CLRガベージコレクタ(GC)によってヒープメモリが管理され、オブジェクトを指す参照が一切なくなってから初めてGCによって回収される。

4. デフォルト値

  • 値型:必ずデフォルト値を持ち、nullにすることはできない(Null許容型を除く)
  • 参照型:デフォルト値はnullであり、スタック上にヒープオブジェクトを指す参照がないことを表す

二、値型の完全な分類

すべての値型は暗黙的に System.ValueType を継承しており、ValueType 自体は object を継承している。

1. 単純な組み込み値型

分類 説明
整数 sbyte、byte、short、ushort、int、uint、long、ulong 固定長数値
浮動小数点 float、double 小数
高精度小数 decimal 財務計算専用
ブール bool true/false
文字 char 単一のUnicode文字

2. 列挙型 enum

基盤は整数型であり、値型に属する

enum Color { Red, Green } // 値型
Color c1 = Color.Red;
Color c2 = c1; // 独立したコピーを作成
c1 = Color.Green; // c2には影響しない

3. 構造体 struct

カスタム値型。フィールド、メソッド、プロパティ、コンストラクタを含めることができる

struct Point // 値型
{
    public int X;
    public int Y;
}

Point p1 = new Point { X = 1, Y = 2 };
Point p2 = p1; // X、Yを完全にコピー
p1.X = 100; 
// p2.X は依然として 1 のままであり、互いに影響しない

注:C# 10以降では引数なしのコンストラクタがサポートされています。構造体にはデフォルトで引数なしのコンストラクタが常に存在し、自動的に0またはデフォルト値が代入されます。

4. ヌル許容値型 Nullable<T> / T?

通常の値型はnullにできませんが、ラップするとnullが可能になります:

int? num = null; // Nullable<int>
if(num.HasValue) {}

値型のメモリ配置図

int a = 10;
int b = a;
a = 20;

スタックメモリ:

スタック:a = 10 → 変更後 20
スタック:b = 10 (独立したコピー、影響を受けない)

三、参照型の完全な分類

すべての参照型は、直接的または間接的に System.Object を継承しており、データはヒープに格納され、スタックには参照アドレスが格納される。

1. クラス class(最も一般的)

カスタム参照型。インスタンスはヒープに割り当てられる

class Person // 参照型
{
    public string Name;
}

Person p1 = new Person { Name = 「張三」 };
Person p2 = p1; // ヒープアドレスをコピーするだけで、同じオブジェクトを指す
p1.Name = 「李四」;
Console.WriteLine(p2.Name); // 「李四」と出力され、

メモリ図解:

スタック:p1 → 0x001(ヒープアドレス)
スタック:p2 → 0x001
ヒープ 0x001:{ Name="張三" } → 「李四」に変更

2. 文字列 string

特殊な参照型で、不変(immutable)

  • classに属し、ヒープ上に存在する;
  • 一度作成されると変更できず、連結や置換を行うと新しい文字列が生成される;
  • 文字列プールの最適化:同一のリテラルはヒープメモリを再利用する。
string s1 = 「abc」;
string s2 = s1;
s1 = 『xyz』; // 新しいヒープオブジェクトが生成されるが、s2は依然として「abc」を指す

3. 配列 Array

要素が値型であろうと参照型であろうと、配列自体は常に参照型である

int[] arr1 = new int[2] {1,2};
int[] arr2 = arr1;
arr1 [0] = 99;
Console.WriteLine(arr2[0]); // 99、配列は共有される

4. インターフェース interface

自体はインスタンス化できませんが、インターフェース変数は参照型であり、実装クラスのオブジェクトのアドレスを格納します。

5. デリゲート delegate、イベント event

本質的にはメソッドポインタをカプセル化したもので、参照型に属する。

6. ダイナミック型 dynamic

内部的にはobjectに基づいており、参照型である。


四、ボクシングとアンボクシング(値型 ↔ object)

1. ボクシング(Boxing):値型 → 参照型

スタック上の値型データをヒープにコピーし、objectとしてラップして、参照を生成する:

int num = 10; // スタック
object obj = num;// ボクシング:ヒープ上にobjectのコピーを作成し、objにはヒープ上のアドレスを格納

オーバーヘッド:ヒープメモリの割り当て、データのコピー。頻繁なボクシングはパフォーマンスに影響する。

2. アンボクシング(Unboxing):object → 値型

ヒープ上のobjectから元の値型データを取得し、スタックにコピーする。この際、型キャストが必須となる:

int num2 = (int)obj; // アンボクシング

誤った例:型の不一致によりInvalidCastExceptionがスローされる。

ボックス化を回避するための最適化

ジェネリックList<T>ArrayListの代わりに使用。ジェネリックコンテナではボックス化やアンボックス化が行われない。


五、混同しやすい重要なポイント

1. struct 対 class:主な使用シーン

struct(値型)で以下の条件をすべて満たす場合:
  1. データサイズが小さい(通常、インスタンスサイズが16バイト未満)
  2. データが軽量で、代入によるコピーがほとんど行われない
  3. 継承やポリモーフィズムが不要
  4. セマンティクスが単一のデータポイント(座標、色、サイズ)である
class(参照型)を使用すべき条件:
  1. データ量が多い場合
  2. オブジェクトの頻繁な受け渡しや共有が必要な場合
  3. 継承、ポリモーフィズム、インターフェースの多重実装が必要な場合
  4. 意味がビジネスエンティティ(ユーザー、注文、商品)である場合

2. ref / out / in パラメータ(値型渡しロジックの変更)

デフォルトでは値型パラメータは値コピーで渡されますが、refを指定すると、スタック変数のアドレスが渡され、メソッド内で変更すると外部変数に影響します:

static void Modify(ref int x)
{
    
x = 999;
}
int a = 10;
Modify(ref a);
// a = 999

3. 読み取り専用構造体 readonly struct

構造体のすべてのフィールドが読み取り専用となり、コピー時にコンパイラが最適化を行い、コピーにかかるオーバーヘッドを削減します。

4. 文字列の特殊な等価比較

  • ==:stringがオーバーロードし、文字列の内容を比較
  • object.ReferenceEquals(s1,s2):同じヒープアドレスを指しているかどうかを比較(文字列プールの再利用を判定)

5. ヌル値の違い

  • 値型int:nullにできない;int?のみnullが許可される
  • 参照型string:デフォルトでnull、ヒープ上のオブジェクトが存在しないことを示す

六、比較まとめ表

を直接継承

比較軸 値型(Value Type) 参照型(Reference Type)
格納場所 スタック(Stack) ヒープ(Heap)、スタック上のアドレス
代入ロジック データの完全なコピー メモリアドレスのみをコピーし、オブジェクトを共有
デフォルト値 数値0、false、\\0、null不可 null(ヒープオブジェクトなし)
メモリ回収 スタックは自動的に解放され、GCなし GCによるマーク・スイープ方式でヒープメモリを回収
継承のルート System.ValueType → object object
代表的な型 struct、enum、int/bool/charなど class、string、配列、デリゲート、インターフェース
変更の伝播 コピー同士は互いに影響しない 一箇所を変更するとすべてが同期される
ボクシング・デボクシング サポートされているが、パフォーマンスの低下を伴う ボクシング不要

7. 完全なデモコード

using System;

// 値型:構造体
struct Point
{
    public int X;
    public int Y;
}

// 参照型:クラス
class Student
{
    public string Name;
}

class Program
{
    static void Main()
    {
        // ========== 値型のデモ ==========
        
Point p1 = new Point { X = 10, Y = 20 };
        Point p2 = p1;
        p1.X = 999;
        Console.WriteLine($「値型 p2.X = {p2.X}」); // 10、影響を受けない
        
// ========== 参照型のデモ ==========
        Student s1 = new Student { Name = 「小明」 };
        Student s2 = s1;
        s1.Name = 『小紅』;
        Console.WriteLine($「参照型 s2.Name = {s2.Name}」); // 小紅、変更が反映される
        
// ========== ボックス化・アンボックス化 ==========        int num = 100;        object boxObj = num; // ボックス化        int unboxNum = (int)boxObj; // アンボックス化        Console.WriteLine($「アンボックス化の結果:{unboxNum}」);    }}

出力:

値型 p2.X = 10参照型 s2.Name = 小紅アンボックス結果:100

8. よくある落とし穴

  1. 構造体をListの要素として使用した場合、直接変更は無効
    List<Point>から取得されるのは構造体のコピーであり、プロパティを直接変更してもコレクション内のデータは変更されません。インデックスを使用して再代入する必要があります。
  2. クラスのnewを頻繁に行うと大量のGCが発生する
    高頻度ループ内でクラスのインスタンスを作成するとヒープの断片化を引き起こすため、構造体やオブジェクトプールを用いて最適化できる。
  3. 文字列の連結はパフォーマンスが低い
    stringは不変であるため、大量の連結にはStringBuilderを使用する。
  4. アンボックス時の型変換エラーで例外がスローされる
    ボックス化された型に対応する型でアンボックスする必要があり、暗黙的な型変換はできません。
  5. 配列は常に参照型です
    たとえ配列の要素がint型であっても、配列そのものを渡す場合は依然として共有されます。