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

C# の yield return で大量データを遅延評価で返す(メモリを食わないイテレータ)

バイブス父さん
現役の業務SE
2026年7月15日8 min read
C# の yield return で大量データを遅延評価で返す(メモリを食わないイテレータ)

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

大量のデータをメソッドから返したい時、みなさんどうしてますか?? とりあえず List に全部詰めて return。これが多いんじゃないでしょうか。

私も長いことそうでした。

でも件数が数十万、数百万と増えてくると話が変わる。その List がまるっとメモリに乗って、地味に重いんですよね。

C# には、これを「1件ずつ、必要な分だけ」返す yield return という書き方があります。今回はこの yield return の使い方を、仕組みから落とし穴まで積み上げて理解していきましょう。

そもそも: List に全部詰めると何が起きるか

まずは、ふつうに List で返すコードから。

public static List<int> GetEvens(int max)
{
    var result = new List<int>();
    for (int i = 0; i <= max; i++)
        if (i % 2 == 0) result.Add(i);
    return result;
}

これ、GetEvens(1000000) を呼ぶと、その瞬間に50万件を全部作って List に載せます

呼び出し側が最初の10件しか要らなくても、です。全部。え、10件でよくない??ってなりますよね。

件数が少なければ何も困りません。でも大量データになると、この「全部作ってから返す」がメモリを圧迫してくる。

ここまでのポイント: List で返すと、呼んだ瞬間に全件がメモリに乗る。

yield return とは何か

yield return は、メソッドを「1件返すたびに一時停止できる」イテレータに変える キーワードです。

ふつうのメソッドは return で値を1回返したら終わり。でも yield return は違う。値を1件返してその場で中断し、呼び出し側が次を要求したら続きから再開します。

この「呼ばれた分だけ動く」性質を 遅延評価(lazy evaluation)と呼びます。

全部先に作らず、必要になった時に初めて1件作る。そういうイメージです。

ここまでのポイント: yield return は「1件返して中断・次で再開」するイテレータを作る。

yield return はどう動くのか

もう少し仕組みを見ていきます。foreach で回すと、裏では MoveNext() が繰り返し呼ばれていて、その1回1回が yield return の1件に対応する。

foreach が MoveNext を呼ぶたびに yield return まで実行して1件返し中断、次の MoveNext で再開する遅延評価の流れ

言葉だけだと分かりにくいので、実際に動かして確かめます。

public static IEnumerable<int> ReadNumbers()
{
    Console.WriteLine("開始");
    for (int i = 1; i <= 3; i++)
    {
        Console.WriteLine($"  {i} を返す直前");
        yield return i;
    }
    Console.WriteLine("終了");
}

// 呼び出し側
var seq = ReadNumbers();                 // ここでは "開始" はまだ出ない
Console.WriteLine("メソッドは呼んだ。でもまだ回してない");
foreach (var n in seq)                   // ここで初めて中身が動き出す
{
    Console.WriteLine($"受け取った: {n}");
}

出力はこうなります。

メソッドは呼んだ。でもまだ回してない
開始
  1 を返す直前
受け取った: 1
  2 を返す直前
受け取った: 2
  3 を返す直前
受け取った: 3
終了

実際に .NET で動かした結果がこれです。

ReadNumbers の実行結果。メソッドを呼んだ直後は開始が出ず、foreach で回して初めて処理が動く遅延評価の順序

ReadNumbers() を呼んだだけでは "開始" が出てない。foreach で回して初めて中身が動いてます。

遅延評価っていうのは、こういうことです。いい感じに1件ずつ処理が進んでますね。

ここまでのポイント: 中身が動くのは呼んだ時じゃなく、回し始めた時。

List で返す版 → yield で返す版

さっきの偶数を返すコードを yield return に書き換えると、こんな感じ。

List に全件詰めて返すメソッドを yield return で1件ずつ返すイテレータに書き換える before/after

戻り値の型が List<int> から IEnumerable<int> に変わって、result の詰め込みが消えました。

呼び出し側は foreach (var n in GetEvens(1000000)) で同じように回せます。書き方は変わらない。

違うのは、回した分しかメモリに乗らないこと。最初の10件で break すれば、残りの50万件近くは作られすらしません。

ここまでのポイント: yield 版は必要な分だけ作る。使う側の書き方は変わらない。

遅延評価だから注意: DB 接続を開いたまま回すと

ここが、業務でいちばん踏みやすい落とし穴です。

遅延評価ということは、中身が動くのは呼び出し側が回している間。DB 接続やファイルを開いて yield return すると、その接続は「回し終わるまで開きっぱなし」になります。

たとえば、こんな感じのコード。

public static IEnumerable<string> ReadFromDb()
{
    using (var conn = OpenConnection())   // 接続を開く
    {
        // reader を回して1行ずつ返す
        foreach (var row in ReadRows(conn))
            yield return row;
    }   // ← 閉じるのは「呼び出し側が回し終わった時」
}

何が起きるか。呼び出し側が1件ごとに重い処理をしてゆっくり回すと、その間ずっと接続が握られたまま。接続プールを長時間占有して、他の処理を待たせる。そういう事故につながります。

誤解しがちなので補足します。foreach で回してる限りは大丈夫です。

途中で break しても例外で抜けても、C# が裏でイテレータの Dispose を呼んでくれるので、using の接続はちゃんと閉じる。

危ないのは、foreach を使わず自分で GetEnumerator() して MoveNext() を回し、Dispose を呼ばずに放置した時。ここだと接続が閉じ切らないことがあります。ん? 自分で回すコード書いてない?って一回立ち止まる価値があるやつです。

ここまでのポイント: yield は「開きっぱなしのリソースを、回し終わるまで握り続ける」。

全部まとめて欲しい時は ToList() で確定させる

遅延評価が向かない場面もあります。「結果を何度も使い回す」「接続を早く閉じたい」「件数がそもそも少ない」。

こういう時は ToList() で確定させます。

// その場で全件読み切って List に確定させる
var rows = ReadFromDb().ToList();   // ここで全件評価 → 接続もすぐ閉じる
// 以降は rows を何度でも使い回せる

ToList() を挟むと、その行で全件を評価してメモリに載せます。遅延評価のメモリ節約は消える。でも代わりに、接続を早く閉じられる・結果を使い回せるというメリットが手に入る。

大量データを1回流すだけなら yield のまま。少量を何度も触るなら ToList()。この使い分けが肝です。

ここまでのポイント: すぐ確定・使い回すなら ToList()。大量を1回流すだけなら yield のまま。

私の現場ではこう使ってる

流通系の基幹システムの保守で、夜間バッチが数百万行の明細を1行ずつ加工して別テーブルに書く、という処理がありました。

最初は全件を List に読んでたんです。これがメモリきつくて。

IEnumerable + yield return で「読みながら書く」形に変えたら、メモリ使用量がガクッと下がりました。全件をいっぺんに抱えないので、当然っちゃ当然。でも効果は目に見えて出ます。

ポイントは、読み込みと書き込みを1件ずつパイプラインにすること。yield はこういう「流しっぱなしの大量処理」と相性がいいです。

まとめ

C# の yield return、こう理解しておけば現場で使えます。

  • yield return は「1件返して中断・次で再開」する遅延評価のイテレータ
  • List と違って、回した分しかメモリに乗らない
  • ただし DB 接続やファイルを開いたまま回すと、閉じるタイミングが回し終わりまでずれる
  • すぐ確定・使い回すなら ToList()、大量を1回流すだけなら yield のまま

遅延評価を押さえると、大量データ処理でメモリを抑える引き出しが1つ増えます。

まずは上の ReadNumbers をコピペして、出力の順番を目で確かめてみてください!!

よくある質問

Q1. yield return は何のために使いますか?

大量のデータを List に全部ためずに、呼び出し側が回した分だけ1件ずつ返すためです。全件をメモリに載せないので、大きなデータや延々続く列を扱う時にメモリを抑えられます。

Q2. yield return と List で返すのはどう違いますか?

List は呼んだ瞬間に全件を作ってメモリに載せます。yield returnforeach で回した分だけ実行され、必要な分しか乗りません。使い回すなら List、1回流すだけ・件数が多いなら yield が向いてます。

Q3. DB 接続やファイルを扱う時の注意点は?

遅延評価なので、開いたまま yield すると呼び出し側が回し終わるまで開きっぱなしになります。回すのが遅いと接続を長く占有します。すぐ確定したいなら ToList() で読み切って早く閉じましょう。

Q4. ToList() するとどうなりますか?

その場で全件を評価して List に確定させます。メモリ節約は消えますが、結果を使い回せる・接続を早く閉じられる、というメリットがあります。用途で使い分けです。

次に読むべき記事

以上!

同じ大量データでハマってる人がいたら、どんどんシェア待ってるぜ!!

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

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

運営者について