動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

C# のボックス化・ボックス化解除とは何か、大量処理で効いてくる理由

バイブス父さん
現役の業務SE
2026年7月17日12 min read
C# のボックス化・ボックス化解除とは何か、大量処理で効いてくる理由

C# のボックス化・ボックス化解除とは何か、大量処理で効いてくる理由

みなさんこんにちは!ヒロポンです!

古い業務システムのコードを触っていると、ArrayList に値を詰めて回すループが「なんか遅いな??」と引っかかることがあります。

同じ処理を List<int> に置き換えると、なぜかスッと速くなる。

この差の正体が ボックス化です。この記事では、C# ボックス化 とは何かを、値型と参照型のメモリの動きから一段ずつ積み上げて説明します。そのうえで、大量処理でパフォーマンスを落とさない書き換えを3つ紹介します。

理屈が腹に落ちると、「どこでボックス化が起きてるか」が読めるようになる。そこがゴールです。

ArrayList や object 引数が「なんか遅い」正体

まず、何が問題なのかを整理します。

intDateTime のような値型を、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行、メモリの中では何が起きているか。図にします。

int の値がボックス化でヒープの箱にコピーされ、object 変数がそれを参照し、ボックス化解除で再び値へコピーされる流れ

注目は B の「ヒープに箱を確保」の部分です。ヒープにオブジェクトを1個作るというのは、GC(ガベージコレクタ)が後で回収する対象を1個増やす、という意味になります。

1個なら誤差。でも100万回ループで100万個の箱を作れば、GC はそのぶん頻繁に走る。これが「大量ループで効いてくる」の中身です。

ここまでで分かったこと:ボックス化はヒープ確保を伴う。回数が増えると GC の仕事が増える。

ボックス化を避ける書き換え3つ

仕組みが分かったところで、実務での回避策です。どれもコピペで試せます。

書き換え1: ArrayList → List

一番よく効くのがこれ。ArrayList は要素を object で持つので、値型を入れるたびにボックス化、取り出すたびにボックス化解除が起きます。List<int> なら int のまま格納するから、ボックス化しません。

ArrayList を List<int> に書き換える before/after。値型の Add と foreach でボックス化・ボックス化解除が消える

見た目はほとんど同じ。違うのは ArrayListList<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: 無意識のボックス化を測って確かめる

「本当にそんな差が出るの??」を体感するために、ArrayListList<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 と List<int> のベンチ実行結果。ArrayList 912 ms に対し List<int> 118 ms

手元で回すと ArrayList が 912 ms、List<int> が 118 ms でした!!7〜10倍くらいの差です。絶対値は .NET のバージョンや環境で変わりますが、大量件数だと ArrayList 側がはっきり重いという傾向は動きません。

ここまでで分かったこと:ArrayList → List、object 引数 → ジェネリック、で無意識のボックス化はほぼ消せる。

いつ効いてくるか:見落としやすい場面

ボックス化は「気づかないうちに起きている」のが厄介です。踏みやすい場面を2つ。

① 非ジェネリックコレクションへの値型追加

ArrayList だけでなく、HashtableQueue(非ジェネリック版)も同じです。レガシーな業務コードには、ジェネリックが無かった時代の名残でこれらが残っている。値型を大量に出し入れしているなら、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> 制約)で値型のまま扱う
  • 気にするのは「大量ループ × 値型」が重なる場所だけ。単発は誤差

仕組みが腹に入っていると、遅い処理を見たときに「ここでボックス化してるな」と当たりを付けられます。まずは手元の ArrayListList<T> に変えて、時間を測ってみてください。

よくある質問

Q1. C# のボックス化とは何ですか?

値型(intDateTime など)を object や interface 型として扱うときに、値をヒープ上の箱にコピーする処理です。逆に箱から値型を取り出すのがボックス化解除です。1回のコストは小さいですが、大量ループで繰り返すと GC への負荷になります。

Q2. ArrayList と List はなぜパフォーマンスが違うのですか?

ArrayList は要素を object で持つため、int などの値型を入れるたびにボックス化、取り出すたびにボックス化解除が起きます。List<T> は要素を T のまま格納するので、値型でもボックス化が起きません。大量データではこの差が効いてきます。

Q3. 無意識にボックス化が起きるのはどんな場面ですか?

ArrayListHashtable などの非ジェネリックコレクションに値型を入れる、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 で経歴詳細を見る


この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について