e1 と e2 は同じ敵だった――var e2 = e1; で学ぶ C# 参照コピーの仕組み

var e2 = e1; は オブジェクトを複製 するのではなく、参照値(アドレス)をコピーするだけ。結果として「Enemy インスタンスは 1 つ、変数は 2 つ」という状態になる。


1. まずはコードを確認

var e1 = new Enemy(); // Enemy インスタンスを生成
var e2 = e1;          // 参照をコピー

上のイラストで、e1 と e2 が同じ Enemy インスタンスを指しているメモリ構造がひと目で分かります。スタック上の変数(e1・e2)は矢印だけを持ち、ヒープ上のオブジェクトは 1 個──まさに「オブジェクトは 1 つ、参照は 2 つ」です。

ステップ実行内容メモリのイメージ
① new Enemy()ヒープ領域に Enemy が 1 つ確保されるヒープ: [Enemy]
② e1 に代入スタック上のローカル変数 e1 に、Enemy への参照値が入るe1 ➜ [Enemy]
③ e2 = e1参照値をコピー。e2 も同じアドレスを指すe1 ➜ [Enemy]e2 ➜ [Enemy]
e1.Health = 10;
Console.WriteLine(e2.Health); // => 10

e1 も e2 も同じオブジェクトを見ているため、片方のプロパティ変更はもう片方にも反映されます。


2. 参照型 vs. 値型 ――この違いがキモ

特性参照型 (class)値型 (struct)
どこに格納?ヒープ(実体) + スタック(参照)スタック or ヒープ(Box化)
代入時の挙動アドレスをコピー内容をコピー
共有可否複数変数で同一インスタンスを共有コピー先は独立した別オブジェクト

値型の代入は“写し”を作るのに対し、参照型は“同じものを指す矢印”を配るイメージです。


3. ガベージコレクションに注意

  • .NET の GC は「参照カウント」ではなく 到達不能判定
  • したがって
e1 = null; // e2 はまだ生きている
  • の場合、Enemy インスタンスは回収されません。すべての参照が途切れて初めて GC の対象になります。

4. デバッグで“同じもの”かどうかを確かめる

  1. ウォッチ式で比較
e1 == e2   // true なら同一インスタンス
  1. ハッシュコードを出力
Console.WriteLine(RuntimeHelpers.GetHashCode(e1));
Console.WriteLine(RuntimeHelpers.GetHashCode(e2));
  1. 2 行が同じ数値を示せば、内部的なアドレス(オブジェクト ID)が同一です。

5. 初学者がハマりやすいポイントと対策

ありがちな誤解実際には…早めに教えたいコツ
e2 = e1 で「コピーしたから別物」という思い込み参照値コピーなので同一オブジェクト図解で“矢印が 2 本”のイメージを示す
e1 を null にすればメモリ解放される他の参照がある限り GC されない“参照が 0 本になって初めて回収”を強調
struct でも同じ動きと思ってしまう値型は 内容コピー(別インスタンス)Point p1 = p2; と p1.X = … の例で違いを実演

6. まとめ

  • 参照型の代入は“アドレス伝言”。オブジェクトの数は増えない。
  • 値型は“実体コピー”。独立した箱ができる。
  • GC は到達不能になったとき初めて回収。1 本でも参照が残っていれば生き続ける。
  • デバッグ時は == 比較や RuntimeHelpers.GetHashCode() で同一インスタンスか確認できる。

付録:ワンポイント練習問題

  1. 下のコードを実行したとき、最後に表示される値は?
var e1 = new Enemy { Health = 100 };
var e2 = e1;
e2.Health = 50;
Console.WriteLine(e1.Health); // ???
  1. 値型 struct Position { public int X; } を使い、p1 と p2 が独立していることを示すコードを書け。
  1. 50…同一インスタンスを指しているため。
  2. p1.X = 1; Console.WriteLine(p2.X); // 0 などで確認。

これで「参照コピーの仕組み」が腑に落ちれば幸いです。

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