イミュータブル値型とボックス化の落とし穴
目次
― “机に直置き”方式でも油断は禁物!
対象: C# の値型 (struct) をもっと安全・高速に使いこなしたい人
この記事のゴール
- イミュータブル値型 を正しく定義し、安心してコピーできるようになる
- ボックス化 (boxing) が発生する場面と性能コストを理解し、回避パターンを身に付ける
1. なぜ値型をイミュータブルにするのか?
可変 (mutable) 値型 | イミュータブル (immutable) 値型 |
---|---|
コピー後にフィールドを書き換えられる | 生成時に完全初期化・以後は変更不可 |
共有ライブラリが思わぬ副作用を起こす恐れ | “一度作ったら壊れない” 安心感 |
たまたま ref で渡されると上書きされ得る | 並行処理・キャッシュにそのまま使える |
✔ ポイント: “机にテレビ本体を複製”しても、ボリュームツマミが動くならコピーが信用できません。
イミュータブル化は “ツマミを溶接” して不変にするイメージ。
実装 3 パターン
パターン | コード例 | 特徴 |
---|---|---|
readonly struct | public readonly struct Point(int x, int y) { public int X { get; } = x; public int Y { get; } = y; } | ・C# 10 以降は record struct が最短・全フィールドが自動 readonly |
伝統的 struct + private set | public 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 何が起こるか?
- 値型 → 参照型 に暗黙変換される
- CLR がヒープに “箱 (object)” を確保
- 値型を丸ごとコピーして箱に詰め、参照を返す
<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 箇条
- 小さく不変なら readonly struct / record struct
- object 化・非ジェネリック利用を疑え:箱行きコストは見えにくい
- 大量データ or 可変データ を値型で持たない:参照型へ逃げる
覚え方
- イミュータブル値型 = 溶接済み前面操作パネル
- ボックス化 = ヒープ倉庫に梱包発送→送料とゴミが増える
訪問数 2 回, 今日の訪問数 1回
ディスカッション
コメント一覧
まだ、コメントがありません