参照型は“実行時”に実体化する

C# でクラス(class)を宣言すると「参照型」と呼ばれますが、コードを書いた瞬間にオブジェクトがメモリに存在するわけではありません。この記事では

  1. 参照型と値型の違い
  2. 実体(オブジェクト)がヒープに確保されるタイミング
  3. 参照のコピーによる “同一インスタンス共有” の挙動

を、最小コードとイラストだけで確認します。

このサンプルでは、ソリューション名として ReferenceTypeRuntimeSampleを推奨します


1. 参照型と値型 ― まずは用語を整理

区分代表例実体が置かれる主な場所コピー時の挙動
値型int, float, structスタック領域(局所変数など)値そのもの を複製
参照型class, string, arrayヒープ領域(実体)+スタック(参照値)参照値 を複製(実体は共有)

ここで言う “参照値” は実体(ヒープ上のオブジェクト)を指し示すポインタのようなものです。


2. 変数宣言だけではヒープに何もない

Person p = null;   // まだオブジェクトなし
Console.WriteLine(p == null);  // True
  • p という変数(スタック上)は確保されます
  • しかしヒープには Person の実体は存在しません
  • p には “参照先がない” ことを示す null が入っています

3. new で初めてヒープにオブジェクトが確保される

p = new Person { Name = "太郎" };
Console.WriteLine(p.Name);  // 太郎
  • CLR がヒープに Person の実体を作成
  • その参照値が p に格納される
  • p を通じてプロパティ Name にアクセス可能

4. 参照をコピーすると“同じ”オブジェクトを共有

Person q = p;   // 参照値をコピー
q.Name = "次郎";

Console.WriteLine(p.Name);  // 次郎
Console.WriteLine(q.Name);  // 次郎
  • 新しいヒープ領域は確保 されません
  • p と q が同一インスタンスを指すため、片方を変更するともう片方にも反映

5. タイミング別まとめ

タイミングメモリで起きていること
コンパイル時Person が「参照型」だと IL(中間言語)へ分類されるだけ
実行時 ①(宣言)Person p = null; → スタックに p を確保し null を格納
実行時 ②(生成)new Person … → ヒープにオブジェクト生成、参照値を p へ
実行時 ③(参照コピー)Person q = p; → 参照値をコピー、ヒープ側は共有

全体のコード

using System;

class Person
{
    public string Name { get; set; }
}

class Program
{
    static void Main()
    {
        // 1. 変数宣言だけでは参照は null
        Person p = null;
        Console.WriteLine($"宣言直後: p == null -> {p == null}");

        // 2. new で初めてヒープにオブジェクトが確保される
        p = new Person { Name = "太郎" };
        Console.WriteLine($"生成直後: p.Name = {p.Name}");

        // 3. 参照のコピー。p と q は同じオブジェクトを指す
        Person q = p;
        q.Name = "次郎";

        Console.WriteLine($"参照コピー後: p.Name = {p.Name}"); // 次郎
        Console.WriteLine($"参照コピー後: q.Name = {q.Name}"); // 次郎
    }
}

実行結果

宣言直後: p == null -> True
生成直後: p.Name = 太郎
参照コピー後: p.Name = 次郎
参照コピー後: q.Name = 次郎
宣言生成直後の値コンパイル時の注意点よく使われる場面
public string Name { get; set; }null(参照型の既定値)C# 8 以降で nullable 参照型機能を有効にしている場合、Name は「null になり得るのに null 許容でない」という警告 (CS8618) が出る。「値がまだ未定・未設定」であること自体が意味を持つとき(たとえばデシリアライズ直後に個別に埋める場合など)。
public string Name { get; set; } = string.Empty;空文字列 (“")警告は出ない(オブジェクト生成時に必ず非 null で初期化)。「未設定でも null では扱いたくない」「UI で空文字列をそのまま表示して問題ない」など、null チェックの手間を省きたい場合。
public string Name { get; set; } = “";空文字列 (“")動作は string.Empty と完全に同じ。IL 上でも同じ定数テーブルを参照する。好み・コーディング規約による。string.Empty の方が「空文字列である」と明示的に読めるという意見もある。

仕組みのポイント

  1. 自動実装プロパティ (auto-property)コンパイラが「匿名のバックフィールド」を作成し、そのフィールドに既定値(参照型なら null)を割り当てる。
  2. 初期化子 (= …) がある場合コンストラクタの先頭で「バックフィールドへ代入するコード」が差し込まれる。したがって オブジェクト生成時に必ず代入が走る ため、null にはならない。
  3. string.Empty と “" の違い
    • どちらも定数値であり、ランタイムコストは同じ。
    • string.Empty は「長さ 0 の文字列」という意図が読み取りやすい。
    • リテラル “" は打鍵数が少なくシンプル。プロジェクトのコーディング規約に従えば OK。

どれを採用するかの目安

目的 / 状況推奨パターン
「未設定」の意味を null で区別したいpublic string? Name { get; set; } など nullable 参照型にするか、あえて初期化子を付けない。
必ず非 null にしたいが初期値は不要= string.Empty; や = “"; を付ける。(またはコンストラクタで別値をセット)
生成時に論理的な既定値があるその値を初期化子で与える。例:= “Unknown";

まとめ

  • 初期化子なし → 既定値 null(nullable 参照型の扱いに注意)
  • = string.Empty / = “" → 生成直後に必ず空文字列で初期化され、null チェックが不要になる
  • string.Empty と “" は実装上同等。可読性・規約の好みで選択すれば十分です。

6. まとめ

  • 参照型は設計図でしかなく、new するまで実体は存在しません
  • 変数に入っているのは実体そのものではなく「参照値」である点が重要
  • 参照値をコピーすれば、どの変数から操作しても同一のオブジェクトに作用します

この基本を押さえておくと、ガベージコレクション や 値型⇔参照型のパフォーマンス差 など、次の学習ステップが理解しやすくなります。ぜひ手元の IDE でサンプルを動かし、イラストと出力結果を見比べながら挙動を確認してみてください。

参考)クラスのコードはどこに格納(保存)されるの?

参考)ブレークパインと

参考)new を 2 回呼ぶとヒープも 2 つ

「q が別インスタンスを指す」シナリオ

上の図では p と q がそれぞれ異なる Person オブジェクトを指しているケースを表しています。

ヒープ領域に 2 個 の Person 実体が並び、スタック上の参照変数は別々のアドレス値(ポインタ)を保持します。


コードで確認

Person p = new Person { Name = "太郎" };  // ヒープ #1
Person q = new Person { Name = "次郎" };  // ヒープ #2(別アドレス)

Console.WriteLine(p.Name);  // 太郎
Console.WriteLine(q.Name);  // 次郎
Console.WriteLine(Object.ReferenceEquals(p, q)); // False
  • new を 2 回呼ぶたびに CLR が ヒープ上へ新規オブジェクトを確保
  • 参照値(アドレス)も当然ながら異なるため、ReferenceEquals は False。

メモリ配置のポイント

領域役割
スタックローカル変数 p, q が保持する参照値(ポインタ)
ヒープPerson インスタンス本体(今回 2 個)
ローダー ヒープclass Person { … } の型メタデータが 1 つだけ存在

つまり

  • クラス定義 はプロセス内に 1 コピー
  • インスタンス は new の回数だけ増える
  • 参照変数が別アドレスを持つことで “ヒープを分ける” 状態になるわけです。

この図とコードを併せて示せば、「参照コピーで共有」 と 「別々に生成して独立」 の違いが視覚的に一目で理解できます。

訪問数 78 回, 今日の訪問数 78回