イミュータブル値型とボックス化の落とし穴

― “机に直置き”方式でも油断は禁物!

対象: C# の値型 (struct) をもっと安全・高速に使いこなしたい人

この記事のゴール

  1. イミュータブル値型 を正しく定義し、安心してコピーできるようになる
  2. ボックス化 (boxing) が発生する場面と性能コストを理解し、回避パターンを身に付ける

1. なぜ値型をイミュータブルにするのか?

可変 (mutable) 値型イミュータブル (immutable) 値型
コピー後にフィールドを書き換えられる生成時に完全初期化・以後は変更不可
共有ライブラリが思わぬ副作用を起こす恐れ“一度作ったら壊れない” 安心感
たまたま ref で渡されると上書きされ得る並行処理・キャッシュにそのまま使える

✔ ポイント: “机にテレビ本体を複製”しても、ボリュームツマミが動くならコピーが信用できません。

イミュータブル化は “ツマミを溶接” して不変にするイメージ。

実装 3 パターン

パターンコード例特徴
readonly structpublic readonly struct Point(int x, int y)
{
public int X { get; } = x;
public int Y { get; } = y;
}
・C# 10 以降は record struct が最短・全フィールドが自動 readonly
伝統的 struct + private setpublic struct Color
{
public byte R { get; init; }
  public byte G { get; init; }
 public byte B { get; init; }
}
・C# 9 init アクセサで「初期化時のみ」代入可・引数なしコンストラクタが使えない点に注意
手動 readonlyフィールドpublic struct Vector2
{
public readonly float X;
public readonly float Y;
 public Vector2(float x, float y)
{
X = x;
Y = y;
}
}
・古い C# でも動くが冗長・プロパティに比べ IDE 補助が弱い

2. ボックス化 (boxing) ― ヒープ行き片道切符

2.1 何が起こるか?

  1. 値型 → 参照型 に暗黙変換される
  2. CLR がヒープに “箱 (object)” を確保
  3. 値型を丸ごとコピーして箱に詰め、参照を返す
<img src="https://user-images.githubusercontent.com/placeholder/boxing_flow.svg" alt="boxing flow" width="480">
発生例典型コード
object への代入object o = 42;
interface の実装先呼び出しIFormattable f = 42;
params object[] 引数Console.WriteLine(“{0}", 42);
非ジェネリックコレクションArrayList list = new(); list.Add(42);

⏰ コスト: ヒープ確保 + コピー + GC 対象 → ミリ秒級フレームでも累積すると響く

2.2 アンボックス化 (unboxing) の罠

object boxed = 42;
int x = (int)boxed;   // アンボックス OK
short s = (short)boxed; // ❌ InvalidCastException
  • アンボックス時は 元の具体的型 でキャストしないと例外
  • 実行中に型が分かるパスを徹底すべし

2.3 回避テクニック

シチュエーションボックス化を防ぐ方法
数値計算ループループ外で object 化された値を作らないSpan<T> / Memory<T> を使う
ログ出力文字列補間 $"Value={value}" で事前に文字列化
コレクションジェネリック (List<int>) を使う非ジェネリックは極力避ける
インターフェース呼び出しin パラメータ + readonly struct でボックス化ゼロ

3. イミュータブル値型 × ボックス化 ― 相性チェック

組合せ問題点ベストプラクティス
readonly struct を頻繁にボックス化毎回ヒープコピー → メリット相殺– ジェネリックで扱う- in / ref readonly で渡す
可変 struct をボックス化箱の中でも書き換え可能 → 意図せぬ副作用– 可変なら参照型に切替- どうしても必要ならコピー後に readonly フィールド化

4. 実践ミニベンチ

var sw = new Stopwatch();

// 値型そのまま
sw.Restart();
int sum = 0;
for (int i = 0; i < 10_000_000; i++) sum += i;
Console.WriteLine($"値型直足し : {sw.ElapsedMilliseconds} ms");

// ボックス化
sw.Restart();
object boxed = 0;
for (int i = 0; i < 10_000_000; i++) boxed = (int)boxed + i;
Console.WriteLine($"毎回boxing : {sw.ElapsedMilliseconds} ms");
  • 普通は 数十倍の差 が出る
  • .NET JIT の最適化でも覆しにくいコスト

5. まとめ ― 値型でも落とし穴を踏まない 3 箇条

  1. 小さく不変なら readonly struct / record struct
  2. object 化・非ジェネリック利用を疑え:箱行きコストは見えにくい
  3. 大量データ or 可変データ を値型で持たない:参照型へ逃げる

覚え方

  • イミュータブル値型 = 溶接済み前面操作パネル
  • ボックス化 = ヒープ倉庫に梱包発送→送料とゴミが増える
訪問数 2 回, 今日の訪問数 1回

C#

Posted by hidepon