Inspect で “見るだけ体験”

― SharpLab と 20 行のコードで「値型」と「参照型」を理解しよう ―


1. なにをする?

  • SharpLab(ブラウザだけで動く .NET プレイグラウンド)にそっくり貼り付けて Run ボタンを押すだけ
  • たった 1 本のプログラムで
    • 値型 int のコピー
    • 参照型 Person のコピーを Inspect で “メモリの中身” として可視化します。
  • IL もアセンブリも一切読まなくて大丈夫。「見る」→「違いを感じる」 が今日のゴールです。

2. まずコードをコピペ!

https://sharplab.io で Code : C# Result: Run を選び、

上部の選択行

下のコードを丸ごと貼り付けてください。

using System;
using SharpLab.Runtime;   // Inspect が入っている名前空間

class Program
{
    static void Main()
    {
        // ★ 値型のコピー
        int a = 5;
        int b = a;     // 値そのものをコピー
        b = 9;

        Inspect.Stack(a);   // スタック上の a
        Inspect.Stack(b);   // スタック上の b

        // ★ 参照型のコピー
        Person p = new Person { Name = "太郎" };
        Person q = p;       // 参照(住所)をコピー
        q.Name = "次郎";

        Inspect.MemoryGraph(p, q);  // オブジェクト間の矢印を可視化
    }
}

class Person
{
    public string Name = "";
}

3. 出力をチェック

スタックとヒープという用語

用語の“日本語らしい”ひと言訳

英語よく使われるカタカナ表記可能な和訳(意訳)ニュアンス
Stackスタック「積み重ね領域」数値や一時データを“上に積んで、上から片づける”場所
Heapヒープ「動的確保領域」/「山(盛り)領域」必要に応じて広さを決めて置く“大きな倉庫”
  • 「スタック」の“stack” は 積み重ねる という動詞から来ています。
  • 「ヒープ」の “heap” は 山盛りに積んだ物 という名詞で、メモリを“好きな所に積む”イメージです。
  • 左側「スタック」:メモを 上に積み上げ → 上から片づける イメージ
  • 右側「ヒープ」:倉庫に 好きな場所へ箱を置き、住所ラベルで探しに行く イメージ

3.1 Inspect.Stack(値型)

Inspect.Stack(a)
┌─ Int32  (4 bytes)
│  05 00 00 00       ← 5
└─

Inspect.Stack(b)
┌─ Int32
│  09 00 00 00       ← 9
└─

ポイント: a と b は別々のバイト列=完全に独立

3.2 Inspect.MemoryGraph(参照型)

Person @000001A2F...
 └─ field Name → String "次郎"
q ───┘

「ref」には 2 つの意味があるので整理しましょう

用語何を指す?今回のサンプルとの関係
参照 (reference)一般名詞としての refヒープ上のオブジェクトまでの “住所メモ”。変数が格納している 4 or 8 byte の数値(ポインタ)Person p = …; Person q = p; で p と q が持っている値そのもの。Inspect.MemoryGraph の矢印が可視化しているのがコレ
ref キーワードメソッド引数修飾子変数を「値ではなく 参照 として」メソッドに渡す C# の構文今回のサンプルには 登場していない(static void Foo(ref int x) のように書くときだけ現れる)

1. 今回の「参照 (reference)」=変数が持つ住所

Person p = new Person { Name = "太郎" };
Person q = p;   // ← このコピーが「参照のコピー」
  • p も q も 中身は「0x00007FFB…」のようなヒープ上のアドレス
  • だから 1 つのオブジェクト(Person インスタンス)を共有する
  • Inspect.MemoryGraph では
Person @00007FFB...
  └─ field Name → String "次郎"
q ───┘
  • という形で 「同じ住所を指している」 ことが矢印で示されます

2. refキーワードとは無関係

void Swap(ref int x, ref int y) { … }
  • これは 引数として “変数そのもの” を渡す ための C# 構文
  • 参照型/値型の区別とは別の次元の話
  • 今回の 20 行サンプルでは ref キーワードを使っていないので、混同しなくて大丈夫 です

まとめ

  • このサンプルで言う「ref」= reference(参照)
    • 変数が保持している “オブジェクトへのポインタ” のこと
    • p, q の中に入っているアドレス値
  • ref キーワード はメソッド呼び出し時の特別な渡し方を指定するもの
    • サンプルには出てこない別機能

「参照型だからコピーは“住所メモ”だけ」という仕組みをまず押さえ、その後 ref キーワードの学習に進むと混乱しません。

ポイント: p と q が 同じアドレス を指す一本の矢印=同じ家を共有


4. どう読めばいい?

型の種類コピー後に起こることInspect での見え方
値型 (int, bool, struct など)データをまるごと複製Inspect.Stack に 2 本の別バイト列
参照型 (class, string, 配列など)ヒープ上のオブジェクトは 1 個だけで変数は「住所メモ」を共有Inspect.MemoryGraph で同じアドレスを指す矢印

覚え方はこれだけ👇

値型 = 中身をコピー

参照型 = 住所をコピー


5. もう一歩だけ試してみよう

コードの末尾に 1 行追加し、ヒープ確保(ボックス化)を覗くこともできます。

object boxed = a;      // int を object に入れてヒープ確保
Inspect.Heap(boxed);   // ← 確保先を直接ダンプ

Run すると System.Int32 が Gen0 ヒープに現れ、

値型でも box するとヒープへ行く ことが体験できます。

部分意味備考
System.Int32CLR 型名。ヒープ上にある boxed された int オブジェクト であることを表す値型 (int) を object へ代入するとヒープにラップされ、参照型として扱われる
0x2273A29F450オブジェクトがヒープ上に配置されている 先頭アドレス(ポインタ)16 進数表記。ガベージコレクタが移動させない限り、このアドレスを起点に 24 B が確保されている

つまり Inspect は

「ヒープのアドレス 0x2273A29F450 に、型 System.Int32 のオブジェクトが存在する」
と教えてくれています。

アドレス表記とエンディアン早わかりメモ

項目内容
表示されるアドレス0x2273A29F450 のように 16 進数で上位バイト→下位バイト順 に列記。人が読むための表記で、エンディアンの影響は受けない
実際のメモリ配列64‑bit 環境はリトルエンディアンなので、RAM 上では 50 F4 29 A2 73 22 00 00 と 下位バイト→上位バイト の順で格納される。
CPU が参照するとき
(Intel/AMD/ARM 共通)
バイト列をリトルエンディアン規則で組み立てて論理アドレス 0x0000002273A29F450 を得る。現在 .NET が公式サポートする主要アーキテクチャ(x86/x64 Intel & AMD、Arm64)は すべてリトルエンディアン
覚え方「画面に出るアドレスは固定、並び順はプラットフォーム依存」 と覚えれば混乱しない。

参考:その 24 B の内訳(64-bit CLR)

オフセットサイズ内容
+0x008 Bオブジェクトヘッダー(同期ブロック索引など)
+0x088 Bメソッドテーブルへのポインタ(System.Int32 の型情報)
+0x104 Bフィールド m_value (= 5)
+0x144 Bパディング(8 B 境界調整)

このように、たった 4 B の整数を boxing しただけで 24 B のヒープ領域が使われることが可視化できます。

スクリーンショットに写っているヒープ・ダンプを読むポイント

オフセットラベルバイト列 (16 進)意味
+0x00header00 00 00 00  00 00 00 00同期ブロックインデックス と予備領域。マルチスレッド用ロック情報などが入るが、未使用なので 0。64-bit CLR では 8 byte 固定
+0x08type handle08 74 EB AF  FA 7F 00 00メソッドテーブルポインタ(Method Table)。このアドレスが “このオブジェクトは System.Int32” であることを CLR に伝える
+0x10m_value05 00 00 00実際の整数値 (5)。リトルエンディアンなので下位バイトから並ぶ
+0x1400 00 00 008 byte 境界に合わせる パディング(Int32 フィールドは 4 byte なので余り 4 byte)

64-bit CLR では ヘッダー 8 byte + タイプハンドル 8 byte + フィールド (4 byte) + パディング (4 byte) = 24 byte

以前の記事で Alloc  Gen0  24 bytes  System.Int32 と報告されたサイズと一致します。


画像内の青枠「m_value」

  • 強調されている 05 00 00 00 が int の実値
  • 値型を boxing すると、この 4 byte のために 最低 24 byte のヒープ領域 が取られる点が可視化できます
    • 8 byte ヘッダー
    • 8 byte メソッドテーブルポインタ
    • 4 byte データ
    • 4 byte パディング

ここから得られる学び

  1. 値型→object に代入 (boxing) すると「オブジェクトヘッダー + メタ情報 + 実データ」の形でヒープに置かれる
  2. 実データはたった 4 byte でも、ガーベジコレクタが扱う最小単位(24 byte)になるため 頻繁な boxing はコスト高
  3. Inspect.Heap は ヘッダー・type handle・フィールドを列挙 してくれるので、メモリの実レイアウトを視覚的に理解できる

この分析を 4.3 節(boxing の説明)に追加しておくと、

「なぜ値型をそのまま使った方がいいのか」

「box を避けるために Span<T> や ref struct が出てきた背景」

など、次の学習ステップへ自然に繋げられます。

Inspect.Heap(boxed)
┌─ System.Int32 @0000025C...
│  Size: 24 bytes  Gen: 0
│  Value: 5
└─

→ 値型 int を object に入れて box すると、ヒープ (Gen0) に System.Int32 オブジェクトが作られ、その内部フィールド m_value に 05 00 00 00(= 5)が格納されていることが一目でわかります。


6. まとめ

  • Inspect は Console.WriteLine 感覚で メモリをのぞき窓 に変える便利ツール。
  • “値そのもの” と “住所メモ” の違いが 数字と矢印 で一目瞭然。
  • あとは 変数名や値を変えて再実行 ⇢ 変化を観察してみよう。

見る → 触る → 納得する

コードは短くても、理解はグッと深まります。

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