---
title: "C#のボックス化とは？起きる場面と割り当てバイト数を.NET 10で実測・回避方法まで解説"
url: "https://www.issoh.co.jp/tech/details/4277/"
published: 2024-11-21
updated: 2026-09-29
categories: ["C# / .NET"]
publisher: "株式会社一創"
---

# C\#のボックス化とは？起きる場面と割り当てバイト数を.NET 10で実測・回避方法まで解説

C#のボックス化（boxing）は、`int`や`struct`などの値型を`object`型やインターフェイス型として扱ったときに、値をマネージドヒープ上の新しいオブジェクトへコピーする変換です。キャストを書かなくても起きるため、`ArrayList`への追加や`struct`の`Equals`、`string.Format`の引数といった普通のコードでも、ボックス化による割り当てが発生します。今回の計測では、`ArrayList`への`int`追加が1回24バイト、`IEquatable<T>`を実装していない`struct`の`Equals`が1回48バイトで、`string.Format`ではボックスに加えて文字列の割り当ても出ました。この記事では、起きる場面ごとの割り当て量を.NET 10.0.12で実測した結果をもとに、どこで起き、どう避けるか、ボックス化解除で何が例外になるかを整理します。

## まとめ

- ボックス化は値型を`object`型・インターフェイス型へ変換する処理で、値はヒープ上の新しいオブジェクトへコピーされます。x64では`int`や`long`の1回が24バイト、`decimal`や`Guid`は32バイトでした（.NET 10.0.12で実測）。
- 起きる主な場面は、`ArrayList`など非ジェネリックコレクション、`object`型の引数、インターフェイス型への代入、`IEquatable<T>`を実装していない`struct`の`Equals`と`Dictionary`のキー、`string.Format`の引数です。
- 回避の基本はジェネリックです。`List<int>`、ジェネリック引数`T`、制約`where T : IComparable<T>`はいずれも割り当て0バイトでした。`struct`は`IEquatable<T>`を実装するか`readonly record struct`にします。
- ボックス化解除（unboxing）は割り当てを伴いません。非Nullable値型への変換では、`null`なら`NullReferenceException`、型違いなら`InvalidCastException`です。`is`パターンで確かめてから取り出します。
- .NET 9以降のJITは、メソッドの外へ出ないと証明できたボックスをスタックに置けます。ただし条件付きの最適化なので、ホットパスでは頼らずに書き方で避けるのが確実です。

## ボックス化とボックス化解除の仕組み

C#の型は、値そのものを持つ値型（`int`・`double`・`bool`・`enum`・`struct`）と、オブジェクトへの参照を持つ参照型（`class`・`string`・配列など）に分かれます。すべての型は最終的に`object`（`System.Object`）から派生しているため、値型も`object`型の変数へ代入できます。このとき参照型の変数が指せるように、ランタイムがヒープにオブジェクトを確保して値をコピーします。これがボックス化です。

```
int i = 123;
object o = i;      // ボックス化（IL では box 命令）
int j = (int)o;    // ボックス化解除（IL では unbox.any 命令）
```

ボックス化は暗黙に行われ、ボックス化解除には明示的なキャストが要ります。ボックスの中身は元の変数とは別のコピーです。インターフェイス経由で値を書き換えても、元の`struct`には反映されません。

```
var c = new Counter();
ICounter ic = c;          // ここでボックス化（コピー）
ic.Increment(); ic.Increment();
Console.WriteLine($"{c.Value} {ic.Value}");   // 出力: 0 2

interface ICounter { int Value { get; } void Increment(); }
struct Counter : ICounter
{
    public int Value { get; private set; }
    public void Increment() => Value++;
}
```

「値型はスタック、参照型はヒープ」という説明をよく見かけますが、正確には値型は変数が置かれた場所に値を直接持ちます。値型のローカル変数も、常にスタックに置かれるとは限りません。JITがレジスタで扱う場合や、ラムダに捕捉されて生成クラスのフィールドになる場合があります。クラスのインスタンスフィールドでは、そのオブジェクト内に値を保持します。ボックス化の本質は「スタックからヒープへの移動」ではなく、「参照で指せる独立したオブジェクトを新しく作ること」にあります。

## ボックス化が起きる場面と割り当て量（.NET 10.0.12で実測）

同じ処理を1000回実行したときのヒープ割り当てを、`GC.GetAllocatedBytesForCurrentThread()`で測りました。環境は.NET SDK 10.0.401（ランタイム10.0.12・LTS）、osx-x64、Releaseビルドで、JITの段階コンパイルが最適化済みコードへ切り替わるまでウォームアップしてから計測しています。入力は0〜999の`int`、IEquatable未実装の`struct`は`int`フィールド2個、`Dictionary`検索は登録済みキーの検索です。`object`引数・ジェネリック引数・インターフェイス経由の比較は、呼び出し先を`[MethodImpl(MethodImplOptions.NoInlining)]`でインライン化させない条件にしています。

| コード                                          | 1000回の割り当て | ヒープ上のボックス数/回 |
| -------------------------------------------- | ---------- | ------------ |
| `ArrayList.Add(i)`                           | 24,000 B   | 1            |
| `List<int>.Add(i)`                           | 0 B        | 0            |
| `object`引数のメソッド                              | 24,000 B   | 1            |
| ジェネリック引数`T`のメソッド                             | 0 B        | 0            |
| `IComparable c = i; c.CompareTo(5)`          | 24,000 B   | 1            |
| `where T : IComparable<T>`で比較                | 0 B        | 0            |
| struct の`Equals`（IEquatable未実装）              | 48,000 B   | 2            |
| struct の`Equals`（IEquatable実装）               | 0 B        | 0            |
| `Dictionary`検索（キーがIEquatable未実装）             | 72,000 B   | 3            |
| `Dictionary`検索（IEquatable実装 / record struct） | 0 B        | 0            |
| struct の`GetHashCode`（未オーバーライド）              | 24,000 B   | 1            |
| `string.Format("{0}", i)`                    | 55,920 B   | 1＋文字列        |
| `$"{i}"`                                     | 31,920 B   | 0＋文字列        |
| `Enum.HasFlag`                               | 0 B        | 0            |
| `(int)o`（ボックス化解除）                            | 0 B        | 0            |

`List<int>`は`new List<int>(1000)`の初期確保（List本体32バイトと内部配列4,024バイト、合計4,056バイト）を除いた`Add`の分です。1個のボックスは`int`・`long`・`byte`で24バイト、16バイトの値を持つ`decimal`と`Guid`で32バイトでした。以下、場面ごとに理由を見ていきます。

### 非ジェネリックコレクションとobject型引数でのボックス化

`ArrayList`や`Hashtable`は要素を`object`として保持するので、`Add(42)`の時点でボックス化されます。自作メソッドでも、引数を`object`型にすると呼び出し側で同じことが起きます。`List<T>`やジェネリックメソッドは値型ごとに専用のコードがJITで生成され、値型のまま受け渡されるため割り当てが出ません。

### インターフェイス型への代入とジェネリック制約

`int`は`IComparable`を実装していますが、`IComparable c = i;`と書くとインターフェイス型の変数が参照を持つ必要があるためボックス化します（1000回で24,000バイト）。同じ比較を`static int Compare<T>(T x, T y) where T : IComparable<T> => x.CompareTo(y);`と書けば0バイトです。インターフェイスの型としての役割は[インターフェースとは？意味と種類・コードと通信の契約を実装目線で解説](/tech/details/17395/)で扱っています。

### structの等価比較とDictionaryのキー

最も気づきにくいのがここです。`Equals`をオーバーライドしていない`struct`は`System.ValueType`の既定実装を使います。Microsoft Learnは、この既定実装が「using reflection to compare the values of every field in the type」（型の全フィールドをリフレクションで比べる）と説明しています。引数が`object`型なので比較対象もボックス化され、実測では1回の比較で2個（48バイト）でした。

`Dictionary<TKey, TValue>`は`EqualityComparer<T>.Default`でキーを比べます。キーが`IEquatable<T>`を実装していなければ`Object.Equals`と`Object.GetHashCode`を使う比較器になるため、検索1回で`GetHashCode`と`Equals`の分として3個（72バイト）のボックスが出ました。`struct`を既定の比較器のまま辞書のキーにするなら、`IEquatable<T>`の実装と`GetHashCode`のオーバーライドを基本にしてください。`IEquatable<T>`だけ実装して`GetHashCode`を放置すると、ハッシュ計算の側で割り当てが残ります（実測で検索1000回あたり24,000バイト）。型を変更できない場合は、`Dictionary`のコンストラクターへ専用の`IEqualityComparer<T>`を渡す方法もあります。リフレクションそのものの仕組みは[リフレクションとは？実行時にクラス情報を操る仕組みを言語横断で解説](/tech/details/15262/)にまとめています。

### string.Formatと補間文字列のボックス化・割り当て量の違い

`string.Format("{0}", i)`は引数を`object`で受けるため、文字列の生成に加えてボックスが1個出ます。C# 10以降で.NET 6以上を対象にすると、補間文字列`$"{i}"`は`DefaultInterpolatedStringHandler`へ変換され、ジェネリックな`AppendFormatted<T>`で値を追記します。.NET公式ブログは「value types will no longer be boxed in order to be appended」と説明しています。実測の差は1000回で24,000バイトで、ちょうどボックス1000個分でした。

### 見えにくい場面：インターフェイス経由のforeachとEnum.HasFlag

`List<int>`を`IEnumerable<int>`型の引数で受けて`foreach`すると、構造体の列挙子`List<int>.Enumerator`がインターフェイス型として返るためボックス化します。.NET 10.0.12の既定設定では0バイトでしたが、JITのオブジェクトのスタック割り当てを`DOTNET_JitObjectStackAllocation=0`で止めると1000回で40,000バイトに戻りました。JITが消しているだけで、コード上はボックス化が起きています。

`Enum.HasFlag`は`System.Enum`のメソッドで引数が`Enum`型のため、かつては呼ぶたびにボックス化していました。.NET Core 2.1でJITの組み込み処理（intrinsic）になり、公式ベンチマークの割り当ては48バイトから0バイトになっています。今回の実測でも最適化済みコードでは0バイトでした。ただしウォームアップを5回にとどめた計測では1000回で48,000バイトと出ており、JITの最適化が済むまでは割り当てが残ります。

## ボックス化のコストが効いてくる条件

Microsoft Learnの.NET Framework向け「パフォーマンスのヒント」は、値型のボックス化が単純な参照代入の「up to 20 times longer」（最大20倍）かかり、ボックス化解除のキャストは代入の「four times as long」（4倍）かかりうると書いています。ただし1回あたりは短い処理で、問題になるのは回数です。1回24バイトでも、毎秒100万回通るループなら毎秒24MBのごみを生み、GCの頻度が上がります。

逆に、画面の初期化や設定の読み込みのように数回しか通らないコードでは、ボックス化の割り当てを測定できても、処理全体への影響は小さいことがあります。ボックス化を気にするのは、プロファイラやベンチマークで割り当てが目立つと確認できたホットパスに絞るのが合理的です。可読性を落としてまで全箇所を潰す必要はありません。ボックス化解除自体は割り当てを伴わず、実測でも0バイトでした。

### .NET 9・.NET 10のJITによるスタック割り当て

.NET 9でJITのオブジェクトのスタック割り当て（object stack allocation）が始まり、参照が現在のメソッドの外へ出ないと証明できる限られたケースで、ボックスをヒープではなくスタックに置けるようになりました。.NET 10ではエスケープ解析の適用範囲が広がっています。前節の`IEnumerable<int>`の列挙子が0バイトになったのはこの効果です。

ただし、ボックスを`object`引数でインライン化されないメソッドへ渡すと参照が外へ出るため、.NET 10.0.12でも24,000バイト/1000回のままでした。段階コンパイルを無効にして動的PGOが働かない構成（`DOTNET_TieredCompilation=0`）では、列挙子のボックスも40,000バイトで残りました。JITの最適化は条件がそろったときの上乗せで、コードの書き方で避けるのが前提です。.NET 10全体の変更点は[.NET 10（LTS）とは？サポート期限・新機能・C# 14と.NET 8/9からの移行を解説](/tech/details/9954/)にあります。

## ボックス化を避ける書き方

### ジェネリックと制約によるボックス化の回避

非ジェネリックコレクションは`List<T>`・`Dictionary<TKey, TValue>`へ置き換えます。`object`やインターフェイス型の引数は、型パラメータと制約に書き換えます。制約を付ければ、インターフェイスのメソッドを値型のまま呼べます。

```
// ボックス化する：インターフェイス型で受ける
static int CompareBoxed(int x, int y) { IComparable c = x; return c.CompareTo(y); }

// ボックス化しない：制約付きジェネリックで受ける
static int Compare<T>(T x, T y) where T : IComparable<T> => x.CompareTo(y);
```

LINQの中間処理でも割り当ては出ます。割り当てそのものを抑えたい場合の選択肢として、[ZLinqとは？C#のゼロアロケーションLINQ｜.NET・Unityでの導入と性能・制約](/tech/details/7424/)も参考になります。

### structの等価比較：IEquatable<T>の実装

Microsoft Learnの`Object.Equals`のガイドラインは、値型に`IEquatable<T>`を実装するよう勧め、「Calling the strongly typed IEquatable<T>.Equals method avoids boxing the obj argument.」と書いています。`Equals(object)`と`GetHashCode`も合わせてオーバーライドし、`==`と`!=`も用意します。

```
readonly struct Point : IEquatable<Point>
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y) { X = x; Y = y; }

    public bool Equals(Point other) => X == other.X && Y == other.Y;
    public override bool Equals(object? obj) => obj is Point p && Equals(p);
    public override int GetHashCode() => HashCode.Combine(X, Y);
    public static bool operator ==(Point a, Point b) => a.Equals(b);
    public static bool operator !=(Point a, Point b) => !a.Equals(b);
}
```

手書きが面倒なら`readonly record struct Point(int X, int Y);`で足ります。コンパイラが`IEquatable<Point>`の実装と`Equals`・`GetHashCode`・`==`を生成し、実測でも`typeof(IEquatable<Point>).IsAssignableFrom(typeof(Point))`は`True`、`Dictionary`の検索は0バイトでした。

### 文字列の組み立ては補間文字列かToString

ログやメッセージの組み立ては、`string.Format`や`string.Concat(object, object)`ではなく補間文字列を使います。単独の値を文字列にするだけなら`i.ToString()`が最も少なく、実測で1000回22,400バイト（文字列本体のみ）でした。

## ボックス化解除の例外とNullable<T>の扱い

### ボックス化解除時のInvalidCastExceptionとNullReferenceExceptionの発生条件

ボックス化解除は、ボックスの中身と同じ値型へのキャストだけが成功します。`int`を包んだボックスを`long`で取り出すと、数値としては変換できそうでも`InvalidCastException`です。`null`を非Nullable値型へ取り出すと`NullReferenceException`になります。Nullable値型への変換では例外にならず、nullになります。ECMA-335（CLIの仕様）の`unbox`命令の規定どおりで、原因が違うので例外の種類も違います。

```
// 以下は各変換を個別に試す例です。例外が出る行を実行すると、後続の行には進みません。
object boxed = 42;
long a = (long)boxed;        // InvalidCastException
long b = (long)(int)boxed;   // OK：int で取り出してから変換する

object? nothing = null;
int c = (int)nothing!;       // NullReferenceException

object day = DayOfWeek.Friday;
int d = (int)day;            // OK：5（enum は基になる整数型で取り出せる）
```

例外の例外として、`enum`のボックスは基になる整数型で取り出せます。ECMA-335が、ボックス化解除した`enum`は基になる型と自由に相互変換できると定めているためです（実測でも`5`が返りました）。

### ボックス化解除前のisパターンによる型確認

中身の型が確実でない`object`は、キャストではなく`is`パターンで型を確かめながら取り出します。型が合わなければ`false`になるだけで、例外は出ません。

```
if (boxed is int value)
{
    Console.WriteLine(value);   // 42
}
```

ボックス同士の比較も注意点です。`object o1 = 1, o2 = 1;`のとき、`o1 == o2`は参照の比較なので`False`、`o1.Equals(o2)`は値の比較なので`True`でした。

### Nullable<T>のボックス化

`int?`（`Nullable<int>`）は特別扱いされます。Microsoft Learnによると、`HasValue`が`false`なら「the boxing operation returns the null reference」、つまりボックスは作られず`null`参照になります。値がある場合は`Nullable<int>`ではなく中の`int`がボックス化されます。実測でも`object o = (int?)5;`の`o.GetType()`は`System.Int32`でした。このため`int?`から作ったボックスは、`int`でも`int?`でも取り出せます。

## 自分のコードでボックス化を見つける方法

まずは疑わしい処理の前後で割り当て量を測ります。今回の表もこの方法で作りました。

```
static long Measure(Action action)
{
    for (int r = 0; r < 3; r++)
    {
        for (int w = 0; w < 60; w++) action();   // 最適化済みコードへ切り替わるまで回す
        Thread.Sleep(300);
    }
    long before = GC.GetAllocatedBytesForCurrentThread();
    action();
    return GC.GetAllocatedBytesForCurrentThread() - before;
}
```

ウォームアップが足りないと、最適化前のコードの割り当てを拾います。5回だけ回して測ったときは、`Enum.HasFlag`が48,000バイト、`IComparable`経由の比較が48,000バイト（十分に回した後は24,000バイト）と、本番と違う数字になりました。継続的に比べるならBenchmarkDotNet（2026年9月29日確認時点のNuGet最新安定版は0.15.8）に`[MemoryDiagnoser]`を付け、結果の`Allocated`列を見ます。ベンチマークの組み方と落とし穴は[マイクロベンチマークとは何か？基本概念と重要性を徹底解説、メリット・デメリットや活用例も紹介](/tech/details/8711/)で扱っています。

コンパイル結果のILに`box`命令があるかを見る方法もあります。ただし、前節のとおりJITが後から消すボックスもあるので、「ILに`box`がある＝実行時に割り当てがある」とは限りません。判断は実測を優先してください。

## よくある質問

### ボックス化とボックス化解除の違いは何ですか？

ボックス化は値型を`object`型やインターフェイス型へ変換し、ヒープ上のオブジェクトに値をコピーする処理で、暗黙に起きます。ボックス化解除はその逆で、`int j = (int)o;`のように明示的なキャストで値を取り出します。割り当てが出るのはボックス化の側だけです。

### C#のobject型とは何ですか？

`object`は`System.Object`の別名で、C#のすべての型の基底型です。参照型なので、値型を`object`型の変数に入れるとボックス化が起きます。何でも入る型として便利ですが、値型を扱う場面ではジェネリックに置き換えるのが基本です。

### List<int>にintを追加するとボックス化しますか？

しません。`List<int>`は内部の`int[]`に値のまま格納し、実測でも`Add`の割り当ては0バイトでした。ボックス化するのは要素を`object`で持つ`ArrayList`で、1回24バイトです。

### record structならIEquatable<T>を自分で書かなくてよいですか？

はい。`record struct`はコンパイラが`IEquatable<T>`と`Equals`・`GetHashCode`・`==`を生成します。今回のintフィールド2個のrecord structでは、`Dictionary`検索の割り当ては0バイトでした。ただし、自動生成される比較・ハッシュ計算は各フィールドの既定比較器を使うため、フィールドの型によっては割り当てが発生します。普通の`struct`をキーにする場合だけ、自分で実装が必要です。

### Unityでもボックス化は同じように起きますか？

ボックス化が起きる場面はC#とCLIの仕様で決まるので、Unityでも同じコードで起きます。ただしUnityはMonoまたはIL2CPPで動き、この記事の数値を測ったCoreCLR（.NET 10）とはJITもGCも違います。.NET 10のスタック割り当てのような最適化は期待せず、ジェネリックと`IEquatable<T>`で避けておくのが確実です。

## 関連記事

- [.NET 10（LTS）とは？サポート期限・新機能・C# 14と.NET 8/9からの移行を解説](/tech/details/9954/)
- [.NET 11の新機能とリリース日｜RC1の現状・C# 15・サポート期限と.NET 10との違い](/tech/details/11045/)
- [ZLinqとは？C#のゼロアロケーションLINQ｜.NET・Unityでの導入と性能・制約](/tech/details/7424/)
- [リフレクションとは？実行時にクラス情報を操る仕組みを言語横断で解説](/tech/details/15262/)

---

出典: [C\#のボックス化とは？起きる場面と割り当てバイト数を.NET 10で実測・回避方法まで解説](<https://www.issoh.co.jp/tech/details/4277/>)（株式会社一創）
