C#のDateOnlyでオブジェクトを再利用すべきか毎回生成すべきか

C#で大量データを扱う処理や日付計算を行う処理を書いていると、「このクラスのインスタンス、ループのたびに new した方がいいのか? それとも 1 個だけ作って DateOnly プロパティだけ上書きして回した方がいいのか?」という悩みにぶつかります。本記事では、.NET のメモリモデルや DateOnly の性質を踏まえて、どちらを選ぶべきかを具体例付きで整理します。

目次

前提シナリオ:DateOnly プロパティを持つクラスをループで使う

まず、今回の議論の前提となる典型的なコードイメージを確認しておきます。
たとえば、CSV 行から日付を読み取って何らかの処理をするクラス DateObject があるとします。


public sealed class DateObject
{
    public DateOnly Date { get; set; }

    // 他にもいくつかのプロパティやメソッドがあるイメージ
}

これをループの中で使うとき、ざっくり次の 2 パターンが考えられます。


// パターンA: 1インスタンスを再利用
var dateObj = new DateObject();
for (var i = 1; i < lines.Length; i++)
{
    dateObj.Date = ComputeDate(lines[i]);
    Process(dateObj); // この反復中だけ利用して保持しない
}

// パターンB: 毎回新しいインスタンスを生成
for (var i = 1; i < lines.Length; i++)
{
    var dateObj = new DateObject
    {
        Date = ComputeDate(lines[i])
    };
    Process(dateObj); // 必要であれば後で保持
}

質問はシンプルです。「どっちが望ましいの?」。しかし答えは一言でいうと用途次第です。

結論の整理:「用途次第」だが軸はシンプル

まず最初に、本記事の中心となる結論を整理しておきます。

  • ループの外にインスタンスを保持しない一時利用で、値型プロパティ(ここでは DateOnly)を上書きするだけなら、1 回だけ生成して再利用する方が割り当てが少なく、GC 負荷も軽い。
  • 各反復が「別々の結果」を表し、後からまとめて使う(リストに溜めるなど)なら、毎回新しいインスタンスを生成するのが正しい設計。

つまり、「オブジェクトとしての状態を 反復ごとに独立して残したいかどうか」が、一番大きな判断軸になります。

オブジェクト再利用 vs 毎回生成を比較する

まずは、再利用と毎回生成をざっくり比較してみましょう。

観点1インスタンス再利用各反復で新規生成
ヒープ割り当て1 回だけ反復回数分だけ増える
GC 負荷少ない傾向(短命オブジェクトが少ない)短命オブジェクトが多いと Gen0 回収が増える
意味(セマンティクス)「作業用バッファ」を上書きして使うイメージ各反復が独立した実体(状態)を持つ
後から区別できるかできない(最後の状態しか残らない)各インスタンスを区別して扱える
コードの読みやすさ慣れていないと少し分かりにくい場合も素直で意図が読み取りやすい
バグの入りやすさスコープ漏れ・参照共有に注意状態が閉じているので安全になりやすい

DateOnly が値型であることの意味

今回のポイントの一つは、DateOnly が 値型(struct) だということです。

  • DateOnly 自体は値型なので、プロパティに代入してもその操作だけでは新たなヒープ割り当ては発生しません。
  • DateObject はクラス(参照型)なので、new DateObject() を呼んだときにヒープに 1 つ割り当てが行われるイメージです。
  • したがって、「DateOnly プロパティを書き換えること」自体が問題なのではなく、「DateObject インスタンスを何回 new するか」が GC 的な観点での焦点になります。

もちろん、プロパティの setter 内部で重い計算やログ出力、イベント発火などをしていれば別問題ですが、単なるフィールド代入だけなら、そこに余計な割り当てはほぼありません。

.NET のメモリ管理から見る「再利用」と「毎回生成」

もう少し踏み込んで、.NET の GC 視点からどう違いが出るかを見てみます。

短命オブジェクトが多いとどうなるか

  • 各反復で新しい DateObject を生成すると、ループ回数が多いほど「短命なオブジェクト」が大量に生まれます。
  • .NET の GC は短命オブジェクトの回収に最適化されていますが、それでも数十万~数百万単位の短命オブジェクトが発生すれば、無視できないコストになります。
  • 特に、I/O や他の重い処理が少なく、純粋に計算だけをしているような高頻度ループでは、割り当ての差が顕在化しやすくなります。

1インスタンス再利用の場合

  • ループの前で 1 回だけ new するので、ヒープ割り当ては 1 回きりです。
  • ループの中では DateOnly プロパティを書き換えるだけなので、GC 観点ではほとんど何も増えません。
  • ただし、このインスタンスを他の場所と共有しない・ループ外に残さないという前提が非常に重要です。これを破ると、バグの温床になります。

どちらが速いかは「ケースバイケース」だが傾向はある

実務的には次のように考えると分かりやすいです。

  • 1 回のループで数百~数千件程度なら、どちらでも大差ないことがほとんど。
  • 数十万~数百万件以上のループで、かつ他の I/O が少ない場合は、再利用パターンの方が有利になることが多い。
  • しかし、可読性・保守性を犠牲にするほど最適化するのは得策ではないので、意味的に正しい方を選び、必要ならベンチマークで確認するのが現実的です。

意味(セマンティクス)から考える設計の軸

性能だけを見て判断すると、つい「全部再利用しよう」となりがちですが、オブジェクト指向では意味(セマンティクス)が最優先です。

使い回しは「作業用バッファ」だと考える

1 インスタンスを再利用するパターンは、イメージとしては次のようになります。

  • 「このオブジェクトはあくまで一時的な作業用で、毎回上書きしても構わない」
  • 「このループの外に出た瞬間、このインスタンスは意味を持たない」

たとえば、「1 行 CSV をパースして DateObject に詰め、それをすぐ別のメソッドに渡すだけで捨てる」といった場合が典型です。

毎回生成は「それぞれが独立した結果」だと考える

一方、毎回新しく生成するパターンは、次のような意味になります。

  • 「各インスタンスは1 行分の結果を表し、後からまとめて扱いたい」
  • 「後で results[i] として取り出したとき、ループ中の状態がそのまま残っていてほしい」

たとえば、後で LINQ で絞り込んだり、画面に一覧表示したり、JSON にシリアライズするといった用途があるなら、当然こちらが正解です。

具体的なコードパターンとその意図

パターンA:ループ専用インスタンスを再利用する

ループの外に結果を保持しない場合の典型例です。


public void ProcessLines(string[] lines)
{
    var dateObj = new DateObject(); // ループ専用の作業用インスタンス

    for (var i = 1; i < lines.Length; i++)
    {
        dateObj.Date = ComputeDate(lines[i]);

        // この反復の処理にのみ dateObj を使う(外へ保持しない)
        HandleDate(dateObj);
    }
}

ポイントは以下のとおりです。

  • dateObj のスコープをメソッド内に閉じ込めておく(フィールドや静的メンバーにしない)。
  • HandleDate 内で、dateObj の参照をどこかに保存しない(コレクションに追加しない、非同期に渡さない)。
  • あくまで「このループを処理するための一時バッファ」として割り切る。

パターンB:各反復で新しいインスタンスを生成し、後で保持する

こちらは、各行の結果を後から参照したい場合です。


public List<DateObject> CollectDates(string[] lines)
{
    var results = new List<DateObject>();

    for (var i = 1; i < lines.Length; i++)
    {
        var dateObj = new DateObject
        {
            Date = ComputeDate(lines[i])
        };

        results.Add(dateObj); // 後で使うので各回で生成
    }

    return results;
}

こういうコードの場合、再利用してしまうと次のようなバグが起こります。

  • リスト内の全要素が同じインスタンスを指してしまう。
  • 結局、最後に代入された DateOnly だけが見えてしまう。
  • 見た目上は「要素数はたくさんある」のに、中身は全部同じ状態というやっかいな事態になる。

この種のバグは気付きにくく、テストデータによっては発見が遅れることも多いので、「後で保持するなら必ず new する」と覚えておくのが安全です。

パターンC:そもそもオブジェクトをやめて値だけ使う

よくよく見ると、「DateObject に包む意味はあるのか?」というケースもあります。ただ日付を計算して次の処理に渡すだけなら、オブジェクト自体をなくす選択肢もあります。


public void ProcessLines(string[] lines)
{
    for (var i = 1; i < lines.Length; i++)
    {
        DateOnly date = ComputeDate(lines[i]); // DateOnly 値だけで処理

        HandleDate(date);
    }
}

値だけで十分な場面では、むしろこのパターンが一番シンプルでバグも起きにくくなります。「クラスを作ること」自体が目的化していないか、一度見直してみると良いポイントです。

IDisposable や外部リソースを持つ場合の注意

今回の DateObject は単なるデータホルダーですが、現実のコードではファイルや DB 接続といった外部リソースを持つオブジェクトを使うことも多いでしょう。

  • 内部で FileStream や SqlConnection を持つクラス
  • HTTP クライアント・ソケット・ハンドルなどのネイティブリソースを持つクラス

このようなクラスは通常 IDisposable を実装しており、再利用するか、毎回生成するかで話が変わってきます。

  • 原則:IDisposable なものは using で確実に破棄する。
  • 正しく再利用するには、スレッドセーフであることやライフサイクルをきちんと設計する必要があります。

DateOnly のような「ただの値」は気軽に再利用できますが、外部リソースを持つクラスは安易に再利用せず、基本は短く使ってすぐ破棄と考える方が安全です。

よくある勘違いと落とし穴

匿名型のプロパティは読み取り専用

匿名型を使って一時的にまとめたい場合に、次のようなコードを書こうとしてコンパイルエラーになることがあります。


var temp = new { Date = DateOnly.FromDateTime(DateTime.Now) };
temp.Date = DateOnly.MinValue; // コンパイルエラー

匿名型のプロパティは読み取り専用で、後から書き換えることはできません。今回のように DateOnly だけを差し替えたい用途には向いていないので、素直にクラスや構造体を定義する方が良いです。

値型プロパティをローカル変数に取り出しても元のオブジェクトは変わらない

もう一つよくある勘違いが、「値型プロパティをローカルに取り出して書き換えたのに、元のオブジェクトに反映されない」というものです。


var obj = new DateObject
{
    Date = new DateOnly(2025, 1, 1)
};

// これは DateOnly のコピー
var date = obj.Date;
date = date.AddDays(1);

// obj.Date は 2025-01-01 のまま

値型はコピーが渡されるので、このようにローカル変数をいじっても元のオブジェクトは変わりません。変更を反映させたい場合は、必ずプロパティに代入し直す必要があります。


var obj = new DateObject
{
    Date = new DateOnly(2025, 1, 1)
};

obj.Date = obj.Date.AddDays(1); // こちらは OK

プロパティ setter の副作用に注意

DateOnly プロパティの setter 内で、以下のような副作用があると、再利用・毎回生成のどちらでも挙動が複雑になります。

  • ログ出力・イベント発火・DB 更新・API コールなど外部 I/O を行う
  • 他のプロパティを書き換えるなど、オブジェクトの状態を大きく変更する
  • 外部のサービスやシングルトンにアクセスする

プロパティの setter はなるべく「値をセットするだけ」に留め、複雑な処理はメソッドに切り出す方が、再利用/新規生成の判断も簡単になります。

スレッド共有・非同期処理での再利用は危険

ループ内インスタンス再利用は、シングルスレッドで完結しているときには便利ですが、次のような場合は特に注意が必要です。

  • Parallel.For や Parallel.ForEach を使っている
  • foreach の中で Task.Run などに渡して非同期で処理している
  • 複数スレッドから同じインスタンスにアクセスしている

こうした場合、1 つのインスタンスを共有してしまうと、他のスレッドの書き込みと競合し、非常に再現性の低いバグにつながります。
マルチスレッドで扱う可能性が少しでもあるなら、「毎回新しいインスタンスを生成して、スレッドごとに閉じた状態にする」方がトラブルを避けやすくなります。

実務での判断フロー:チェックリストで決める

ここまでの話を、実務で使いやすいようにチェックリスト形式で整理してみます。

質問Yes の場合No の場合
各反復の結果を後で区別して保持したいか?毎回新規インスタンス生成を選ぶ再利用も検討できる
インスタンスを他のスレッド・非同期タスクと共有するか?安全のため毎回生成(または同期化)シングルスレッドなら再利用しやすい
プロパティの setter に副作用があるか?設計を見直し、副作用をメソッド側に寄せる再利用しても読みやすい設計にしやすい
ループ回数は非常に多く、割り当てコストが気になるか?再利用パターン+ベンチマークで検証まずは分かりやすさ優先で良い
オブジェクト自体が本当に必要か?必要なら意味づけを明確にする値(DateOnly)だけに簡略化する

迷ったときは、次の優先順位で考えると実務的です。

  1. 正しいモデル化・意味の一貫性があるか
  2. コードがチームメンバーにとって読みやすいか
  3. それでも性能がボトルネックになるなら実測して最適化

典型的なユースケース別のおすすめパターン

よくあるシナリオ別に、どちらのパターンが向いているかをざっくりまとめると次のようになります。

ユースケース説明おすすめパターン
CSV を読み込んでその場で集計するだけ集計が終われば行ごとのデータは不要1 インスタンス再利用または値のみ
画面に一覧表示するためのモデル作成後で各行のデータを参照する毎回新規インスタンス生成
外部 API に送るリクエストオブジェクト各リクエストが独立している原則毎回新規生成(誤再利用防止)
内部だけで使う一時計算用オブジェクトメソッド内で完結し、外に出さない1 インスタンス再利用が有効
マルチスレッドで並列処理するデータ各スレッドで独立して扱う必要があるスレッドごとに新規生成(共有しない)

ベンチマークで「本当に差があるか」を確認する

「再利用の方が速いはず」「いや、GC が賢いから差は出ない」など、感覚だけで議論しても結論は出ません。
.NET では BenchmarkDotNet を使って簡単に性能比較ができるので、気になる場合は小さなベンチマークを書いて実測するのがおすすめです。


[MemoryDiagnoser]
public class DateObjectBenchmarks
{
    private readonly string[] _lines;

    public DateObjectBenchmarks()
    {
        _lines = Enumerable.Range(0, 100_000)
            .Select(i => $"2025-01-{(i % 30) + 1:D2}")
            .ToArray();
    }

    [Benchmark]
    public void ReuseInstance()
    {
        var obj = new DateObject();

        for (var i = 0; i < _lines.Length; i++)
        {
            obj.Date = DateOnly.Parse(_lines[i]);
            Consume(obj);
        }
    }

    [Benchmark]
    public void CreateEveryTime()
    {
        for (var i = 0; i < _lines.Length; i++)
        {
            var obj = new DateObject
            {
                Date = DateOnly.Parse(_lines[i])
            };
            Consume(obj);
        }
    }

    private void Consume(DateObject obj)
    {
        // 何もしないダミー処理
    }
}

実測してみると、

  • 大量ループでは再利用の方が割り当て・GC の面で有利
  • 現実的な件数では差が誤差レベルに収まることも多い

など、コードや環境によって結果が変わることが分かるはずです。
だからこそ、「まずは意味として正しい方を選び、必要なら測る」というスタイルが重要になります。

再利用パターンを採用するときの実務上のコツ

「今回はループ専用の一時インスタンスだから再利用でいこう」と決めたとき、いくつか気を付けると保守性が上がります。

スコープをできるだけ狭くする

  • フィールドや静的変数ではなく、メソッド内のローカル変数として持つ。
  • 必要であれば、さらにブロックで囲ってスコープを絞ってもよい。

public void ProcessLines(string[] lines)
{
    {
        var obj = new DateObject(); // このブロック内だけで有効

        for (var i = 0; i < lines.Length; i++)
        {
            obj.Date = ComputeDate(lines[i]);
            HandleDate(obj);
        }
    }

    // ここから先では obj は見えない(誤用を防げる)
}

状態をリセットするメソッドを用意する

DateOnly 以外にも複数のプロパティを持つクラスの場合、前の反復の値が残っているとバグの原因になります。
その場合、初期状態に戻すメソッドを定義しておくと再利用しやすくなります。


public sealed class DateObject
{
    public DateOnly Date { get; set; }
    public int Count { get; set; }
    public string? Note { get; set; }

    public void Reset()
    {
        Date = default;
        Count = 0;
        Note = null;
    }
}

var obj = new DateObject();

for (var i = 0; i < lines.Length; i++)
{
    obj.Reset();
    obj.Date = ComputeDate(lines[i]);
    // 他のプロパティもこの反復専用の値をセットする
}

まとめ:DateOnly プロパティの上書きとオブジェクトのライフサイクルの考え方

最後に、本記事のポイントを整理します。

  • 結論は用途次第だが、軸はシンプル。
  • DateOnly は値型なので、プロパティを書き換えるだけでは追加のヒープ割り当ては発生しない。
  • ループの外にインスタンスを保持しない一時利用で、値型プロパティの上書きだけなら、1 インスタンス再利用は割り当てが少なく GC にも優しい。
  • 各反復が独立した結果として後で活用されるなら、毎回新しいインスタンスを生成するのが正しい設計。
  • 匿名型のプロパティは読み取り専用であり、後から書き換えることはできない。
  • 値型プロパティをローカル変数に取り出しても、元のオブジェクトは変わらない。変更したいならプロパティに代入し直す。
  • IDisposable や外部リソースを持つクラスでは、安易な再利用は避け、ライフサイクル設計と破棄を優先する。
  • マルチスレッド・非同期処理では、1 インスタンス再利用は競合の温床になりやすいので、各タスクごとにインスタンスを分けるのが安全。
  • 最適化が必要だと思ったら、BenchmarkDotNet などで実測してから判断する。

オブジェクトを再利用するか、毎回生成するかという問題は、一見すると「パフォーマンスの話」に見えますが、本質はオブジェクトの意味とライフサイクルをどう設計するかという話です。
まずは「このインスタンスは何を表していて、どこまで生きていてほしいのか?」を言語化し、その上で DateOnly のような値型の特性や GC の振る舞いを踏まえて選択すると、実務でも納得感のあるコードを書きやすくなります。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次