みなさんこんにちは!ヒロポンです!
大量のデータをメソッドから返したい時、みなさんどうしてますか?? とりあえず 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件に対応する。

言葉だけだと分かりにくいので、実際に動かして確かめます。
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 で回して初めて中身が動いてます。
遅延評価っていうのは、こういうことです。いい感じに1件ずつ処理が進んでますね。
ここまでのポイント: 中身が動くのは呼んだ時じゃなく、回し始めた時。
List で返す版 → yield で返す版
さっきの偶数を返すコードを yield return に書き換えると、こんな感じ。

戻り値の型が 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 return は foreach で回した分だけ実行され、必要な分しか乗りません。使い回すなら List、1回流すだけ・件数が多いなら yield が向いてます。
Q3. DB 接続やファイルを扱う時の注意点は?
遅延評価なので、開いたまま yield すると呼び出し側が回し終わるまで開きっぱなしになります。回すのが遅いと接続を長く占有します。すぐ確定したいなら ToList() で読み切って早く閉じましょう。
Q4. ToList() するとどうなりますか?
その場で全件を評価して List に確定させます。メモリ節約は消えますが、結果を使い回せる・接続を早く閉じられる、というメリットがあります。用途で使い分けです。
次に読むべき記事





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




