C# のボックス化・ボックス化解除とは何か、大量処理で効いてくる理由
みなさんこんにちは!ヒロポンです!
古い業務システムのコードを触っていると、ArrayList に値を詰めて回すループが「なんか遅いな??」と引っかかることがあります。
同じ処理を List<int> に置き換えると、なぜかスッと速くなる。
この差の正体が ボックス化です。この記事では、C# ボックス化 とは何かを、値型と参照型のメモリの動きから一段ずつ積み上げて説明します。そのうえで、大量処理でパフォーマンスを落とさない書き換えを3つ紹介します。
理屈が腹に落ちると、「どこでボックス化が起きてるか」が読めるようになる。そこがゴールです。
ArrayList や object 引数が「なんか遅い」正体
まず、何が問題なのかを整理します。
int や DateTime のような値型を、object 型や ArrayList に入れると、C# は裏で「値をヒープにコピーして箱に入れる」処理を走らせています。これがボックス化。
1回あたりのコストは小さい。だから単発なら気にしなくていい。問題は、大量ループの中で無意識に繰り返されるとき。塵も積もれば、で効いてきます。
ここまでで分かったこと:値型を object 扱いすると裏でコピーが走る。単発は無害、大量ループで効く。
ボックス化以前に:値型と参照型はメモリのどこにいるか
ボックス化を理解するなら、その手前で値型と参照型の置き場所を押さえておきます。
- 値型(
int/double/bool/struct/DateTimeなど):値そのものがその場に置かれる。ローカル変数ならスタック上に載る - 参照型(
class/string/ 配列 /objectなど):実体はヒープにあって、変数はそこへの参照(アドレス)を持つ
C# はこの2つを区別して扱います。値型は軽くて速い。その代わり、object として扱おうとすると「参照型の世界」に持ち込む変換が要る。この変換がボックス化です。
ここまでで分かったこと:値型はその場に値が載る。object(参照型)として扱うには変換が要る。
ボックス化・ボックス化解除とは
用語を1文で定義しておきます。
ボックス化(boxing)とは、値型の値をヒープ上に確保した箱にコピーして、object や interface 型として参照できるようにする処理です。 逆に、その箱から値型を取り出してコピーし直すのがボックス化解除(unboxing)です。
コードで見ると、たった2行で両方が起きます。
int n = 42;
object boxed = n; // ボックス化:ヒープに箱を作って 42 をコピー
int m = (int)boxed; // ボックス化解除:箱から 42 を取り出してコピー
object boxed = n; の時点で、42 という値がまるごとヒープにコピーされています。boxed はその箱を指しているだけ。元の n とは別物になっている。ここがポイントです。
ここまでで分かったこと:boxing は「値→箱にコピー」、unboxing は「箱→値にコピー」。どちらもコピーが走る。
ボックス化の仕組み(メモリの動き)
さっきの2行、メモリの中では何が起きているか。図にします。

注目は B の「ヒープに箱を確保」の部分です。ヒープにオブジェクトを1個作るというのは、GC(ガベージコレクタ)が後で回収する対象を1個増やす、という意味になります。
1個なら誤差。でも100万回ループで100万個の箱を作れば、GC はそのぶん頻繁に走る。これが「大量ループで効いてくる」の中身です。
ここまでで分かったこと:ボックス化はヒープ確保を伴う。回数が増えると GC の仕事が増える。
ボックス化を避ける書き換え3つ
仕組みが分かったところで、実務での回避策です。どれもコピペで試せます。
書き換え1: ArrayList → List
一番よく効くのがこれ。ArrayList は要素を object で持つので、値型を入れるたびにボックス化、取り出すたびにボックス化解除が起きます。List<int> なら int のまま格納するから、ボックス化しません。

見た目はほとんど同じ。違うのは ArrayList か List<int> かだけ。それだけで100万回ぶんのボックス化が丸ごと消えて、いい感じに軽くなります。
書き換え2: object 引数 → ジェネリック
object 型の引数も、値型を渡すたびにボックス化します。ジェネリックにすれば、値型のまま受け取れる。
// object 引数:値型を渡すたびにボックス化
bool AreEqual(object a, object b) => a.Equals(b);
AreEqual(1, 1); // 1 が2回ボックス化される
// ジェネリック + 型制約:ボックス化しない
bool AreEqualG<T>(T a, T b) where T : IEquatable<T> => a.Equals(b);
AreEqualG(1, 1); // int のまま、IEquatable<int>.Equals が呼ばれる
where T : IEquatable<T> の制約がミソです。これを付けると a.Equals(b) が int 用の Equals を直接呼ぶので、こんな感じで箱を作らずに済みます。制約なしだと object.Equals(object) 経由になって、結局ボックス化してしまう。
書き換え3: 無意識のボックス化を測って確かめる
「本当にそんな差が出るの??」を体感するために、ArrayList と List<int> を実際に回して時間を測ってみます。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
class Program
{
static void Main()
{
const int N = 10_000_000;
var sw = Stopwatch.StartNew();
var arr = new ArrayList();
for (int i = 0; i < N; i++) arr.Add(i); // 毎回ボックス化
long sum1 = 0;
foreach (int x in arr) sum1 += x; // 毎回ボックス化解除
sw.Stop();
Console.WriteLine($"ArrayList: {sw.ElapsedMilliseconds} ms (sum={sum1})");
sw.Restart();
var list = new List<int>();
for (int i = 0; i < N; i++) list.Add(i); // ボックス化なし
long sum2 = 0;
foreach (int x in list) sum2 += x;
sw.Stop();
Console.WriteLine($"List<int>: {sw.ElapsedMilliseconds} ms (sum={sum2})");
}
}
実行結果(1000万件・.NET 9 のコンテナで計測。絶対値は環境依存です):

手元で回すと ArrayList が 912 ms、List<int> が 118 ms でした!!7〜10倍くらいの差です。絶対値は .NET のバージョンや環境で変わりますが、大量件数だと ArrayList 側がはっきり重いという傾向は動きません。
ここまでで分かったこと:ArrayList → List
、object 引数 → ジェネリック、で無意識のボックス化はほぼ消せる。
いつ効いてくるか:見落としやすい場面
ボックス化は「気づかないうちに起きている」のが厄介です。踏みやすい場面を2つ。
① 非ジェネリックコレクションへの値型追加
ArrayList だけでなく、Hashtable や Queue(非ジェネリック版)も同じです。レガシーな業務コードには、ジェネリックが無かった時代の名残でこれらが残っている。値型を大量に出し入れしているなら、List<T> / Dictionary<TKey,TValue> 系に置き換えるだけで軽くなります。
② struct を interface 経由で呼ぶ
自作の struct が interface を実装している場合、その struct を interface 型の変数に入れた瞬間にボックス化します。値型のつもりで軽く使っていた struct が、interface 経由で大量に扱われていると、静かにヒープを圧迫していく。
「値型なのに GC がやたら走る」ときは、この2つを疑うと当たりやすいです。
私の現場ではこう意識している
正直、単発の処理でボックス化を気にする必要はほぼありません。1回や2回の箱なんて誤差です。
意識するのは、「大量ループ」と「値型」が重なる場所だけ。バッチで数百万件を回す、みたいな処理を書くときに、「ここ、object 経由になってないか?」と一度だけ確認する。それで十分です。
私自身、普段は List<T> とジェネリックを素直に使っていれば、ボックス化はほとんど踏みません。逆に、古いコードで ArrayList を見かけたら「大量に回ってないか」だけチェックする。この程度の意識で、大量処理のパフォーマンスはいい感じに安定します。
まとめ
C# のボックス化、要点を積み上げ直すとこうです。
- ボックス化は値型を object 扱いする時にヒープの箱へコピーする処理。ボックス化解除はその逆
- 箱はヒープに確保されるので、大量に作ると GC 圧になる
ArrayList/ 非ジェネリックコレクションは値型の出し入れで毎回ボックス化。List<T>なら起きない- object 引数よりジェネリック(必要なら
IEquatable<T>制約)で値型のまま扱う - 気にするのは「大量ループ × 値型」が重なる場所だけ。単発は誤差
仕組みが腹に入っていると、遅い処理を見たときに「ここでボックス化してるな」と当たりを付けられます。まずは手元の ArrayList を List<T> に変えて、時間を測ってみてください。
よくある質問
Q1. C# のボックス化とは何ですか?
値型(int や DateTime など)を object や interface 型として扱うときに、値をヒープ上の箱にコピーする処理です。逆に箱から値型を取り出すのがボックス化解除です。1回のコストは小さいですが、大量ループで繰り返すと GC への負荷になります。
Q2. ArrayList と List はなぜパフォーマンスが違うのですか?
ArrayList は要素を object で持つため、int などの値型を入れるたびにボックス化、取り出すたびにボックス化解除が起きます。List<T> は要素を T のまま格納するので、値型でもボックス化が起きません。大量データではこの差が効いてきます。
Q3. 無意識にボックス化が起きるのはどんな場面ですか?
ArrayList や Hashtable などの非ジェネリックコレクションに値型を入れる、object 型の引数に値型を渡す、struct を interface 経由で呼ぶ、string.Format の引数に値型を渡す、などです。単発なら問題ありませんが、大量ループの中だと積み重なります。
Q4. ボックス化を避けるにはどうすればいいですか?
ArrayList ではなく List<T>、object 引数ではなくジェネリック(必要なら型制約つき)を使います。値型の等価比較は IEquatable<T> を実装して制約に使うと、interface 経由のボックス化を避けられます。
次に読むべき記事





以上!
同じところで詰まった人いたら、どんどんシェア待ってるぜ!!
執筆者
バイブス父さん — 業務 SE 7 年(正社員 2 / フリーランス 5)。現職は SEO 直轄部の AI アドバイザー兼 PL、副業で中小 SIer の CTO。SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」の技術 + キャリア記事を書いています。
🐦 X: @hiro_progra0524(日々の現場メモ更新中)
📝 About Me で経歴詳細を見る



