日程Fit|「いつ空いてますか?」の往復はもう不要。候補日を選んでURLを送るだけ|登録不要|今すぐ無料で使う →

C# yield returnとは?遅延実行のメリット・速度の考え方と並列との違いを徹底解説

C#のyieldは「処理を並列化して速くする魔法」ではありません。IEnumerable<T>を1件ずつ返し、呼び出し側が次を要求したときだけ続きを計算するための仕組みです。遅延実行で無駄を減らし、メモリや応答性を改善できる一方、ハマりやすい注意点もあるため、仕組みと使いどころを押さえておくのが重要です。

日程Fit。無料・登録不要。「いつ空いてる?」を、ひとつのリンクで。リンクを送って、○△×でかんたん日程調整。無料で日程を作る。
目次

結論:C# の yield は「並列」ではなく「遅延実行で1件ずつ返す」ためのキーワード

C# の yield return は、メソッドを イテレーター(iterator)として扱い、IEnumerable<T> を“必要な分だけ”生成するために使います。

  • 目的:遅延実行(deferred execution)/遅延評価(lazy evaluation)
  • 結果:呼び出し側(foreach や LINQ)が「次の要素」を要求したタイミングで、メソッドが続きから再開される
  • 誤解されがち:取得と処理が交互に見えるため「並列っぽい」と感じるが、同時実行ではなく逐次

つまり、yieldは「列挙(enumeration)というインターフェースに沿って、要素をストリームのように流す」ための仕組みです。

yield return の仕組み:コンパイラが状態マシン(state machine)を生成する

yield return を含むメソッドは、コンパイル時に内部的に IEnumerable<T> / IEnumerator<T> を実装したクラスへ変換されます。ポイントは以下です。

ポイント何が起きているか体感としてどう見えるか
遅延実行メソッド呼び出し時点では全処理しない。列挙(MoveNext())のたびに進む。「呼んだのに動いてない?」→ foreach開始で動く
一時停止と再開yield returnに到達すると値を返して停止。次の要求で続きから再開。「1件ずつ返してくれる」
状態保持ローカル変数や現在位置は、生成された状態マシンに保持される。ループの途中から自然に続く
後片付け列挙が終わる/中断されるとDispose()が呼ばれ、finallyが実行される(※条件あり)。foreachbreakしても基本は片付く

実際のイメージを短いコードで確認します。

public static IEnumerable<int> RangeLazy(int start, int count)
{
    for (int i = 0; i < count; i++)
    {
        // ここで「1件返して一旦停止」
        yield return start + i;
        // 次が要求されたらここから再開してループ継続
    }
}

呼び出し側が全部を必要としない場合、残りを作りません。

// 先頭3件だけ欲しい
foreach (var x in RangeLazy(10, 1_000_000).Take(3))
{
    Console.WriteLine(x);
}

この場合、100万件分の計算はされず、3件で止まります。これが 遅延実行の強みです。

「並列に見える」理由:取得と処理が交互になるだけ(同時には動かない)

yieldを使うと、呼び出し側が1件受け取るたびに処理するため、ログが「交互」に出て並列っぽく見えることがあります。しかし、実際には 同一スレッドで順番に進んでいるだけです。

例:Listで全件作って返す版

public static List<int> GetOrdersAll()
{
    var list = new List<int>();
    Console.WriteLine("GetOrder(1)");
    list.Add(1);
    Console.WriteLine("GetOrder(2)");
    list.Add(2);
    Console.WriteLine("GetOrder(3)");
    list.Add(3);
    return list;
}

public static void ProcessOrder(int id)
{
Console.WriteLine($"ProcessOrder({id})");
}

foreach (var id in GetOrdersAll())
{
ProcessOrder(id);
}

ログの印象:全部取得→その後まとめて処理

GetOrder(1)
GetOrder(2)
GetOrder(3)
ProcessOrder(1)
ProcessOrder(2)
ProcessOrder(3)

例:yield return

public static IEnumerable<int> GetOrdersLazy()
{
    Console.WriteLine("GetOrder(1)");
    yield return 1;


Console.WriteLine("GetOrder(2)");
yield return 2;

Console.WriteLine("GetOrder(3)");
yield return 3;


}

foreach (var id in GetOrdersLazy())
{
ProcessOrder(id);
}

ログの印象:取得→処理→取得→処理…

GetOrder(1)
ProcessOrder(1)
GetOrder(2)
ProcessOrder(2)
GetOrder(3)
ProcessOrder(3)

この「交互に見える」現象が、yieldが並列・Taskっぽいと誤解される最大の原因です。ですが、これは逐次処理の並び順が変わっただけで、同時実行(並列)にはなっていません。

yield は速くなる?パフォーマンスの本音(“速くなることもある”が目的はそこではない)

yieldは「必ず速くなる」ための機能ではありません。むしろ、要素を1件返すたびに状態マシンの処理が入り、ケースによっては 全件をList<T>で作って返すより遅くなることもあります。

それでもyieldが強いのは、次のように「総仕事量を減らせる」場合です。

  • 途中で打ち切れるbreakFirst()Any()Take(n)など)
  • 巨大なデータを一時リストにしない(メモリ節約、GC負荷軽減)
  • 早く最初の結果が欲しい(体感速度・応答性の向上)

よくあるケース別:yieldList の向き不向き

状況yieldが向くList<T>が向く理由(実務の観点)
先頭の数件だけ必要必要分だけ生成して打ち切れる
全件を1回だけ走査どちらも可能。微差はデータ量と処理内容次第
全件を複数回使い回すIEnumerableは列挙のたび再実行されることが多い
巨大データ(メモリ厳しい)一時コレクションを作らずに流せる
CPUが軽く、要素数が多いyieldの1要素あたりオーバーヘッドが相対的に目立つことがある
UIで「固まらない」ことが重要○〜◎最初の結果を早く返して段階的に処理しやすい

パフォーマンスを語るときのコツは、「速いか遅いか」ではなく「無駄をしないか」「メモリを食わないか」「早く結果が出るか」で考えることです。特に検索系やフィルタリング系は、yieldの恩恵が出やすい代表例です。

測るなら「ベンチマークで」:体感では誤判定しやすい

小さなサンプルやログの見え方だけで結論を出すと、誤解しがちです。計測するなら、BenchmarkDotNetなどで「実データ量」「実際の処理内容」に近い条件で比較するのが安全です。

メリット:yield が本領発揮する場面

必要な分だけ処理できる(打ち切りで無駄ゼロに近づく)

たとえば「条件に合う最初の1件だけ欲しい」「上位10件だけ表示したい」など、現場でよくある要求はyield向きです。

// 条件に合う最初の1件だけ
var hit = GetCandidatesLazy().FirstOrDefault(x => x.IsMatch);

// 上位10件だけ
var top10 = GetCandidatesLazy().Take(10).ToList();

ここで大事なのは、GetCandidatesLazy()yieldなら、必要以上の候補生成が走らない可能性が高いことです(ただし内部実装次第なので、設計者として“止められる形”にしておくのがポイントです)。

体感性能(応答性)が良くなる:全部揃う前に処理を開始できる

「全件が揃うまで待ってから画面更新」より、「取れた分から順次描画・処理」の方が、ユーザーは待ち時間を短く感じます。yieldはこのストリーミング的な処理を自然に書けます。

ただし、UIアプリで本当に“固まり”を減らしたいなら、yieldだけでは不十分です。UIスレッドを塞がないために、I/O待ちや重い処理には async/await を組み合わせる必要があるケースも多いです(後述の IAsyncEnumerable が関連します)。

メモリ節約:巨大な一時リストを作らない

List<T>に全件を入れると、その分のメモリが必要になります。yieldなら、基本は「現在の要素と状態」だけで済むため、データサイズが大きいほど効いてきます。

コードが読みやすくなる:自前の IEnumerator 実装が不要

昔ながらにIEnumeratorを手で実装すると、状態管理・例外・終了処理などが面倒です。yieldなら、ループを書くだけで読みやすく安全な形になりやすいのが大きなメリットです。

注意点・落とし穴:ここを知らないと事故りやすい

IEnumerableは「列挙するたびに再実行」されることが多い

yieldで返したシーケンスは、foreachやLINQで列挙するたびに、基本的に生成ロジックが再度走ると考えておくべきです。

var seq = GetOrdersLazy();

// 1回目の列挙
foreach (var x in seq) { /* ... */ }

// 2回目の列挙(また GetOrdersLazy が走る)
foreach (var x in seq) { /* ... */ }

これが問題になる代表例:

  • DBアクセス、HTTP取得、ファイル読み込みなどの副作用・I/Oが含まれる
  • 乱数や時刻など、列挙ごとに結果が変わる
  • 重い計算を毎回繰り返してしまう

同じ結果を何度も使うなら、一度具体化(materialize)してしまうのが安全です。

var cached = GetOrdersLazy().ToList(); // ここで一度だけ実行して固定
// 以降は cached を何度でも使える

例外が「メソッド呼び出し時」ではなく「列挙中」に発生する

yieldは遅延実行なので、例外が投げられるタイミングも遅れます。つまり、次のようなことが起きます。

  • var seq = GetSomethingLazy(); の時点では例外が出ない
  • foreachが始まって途中で突然例外が出る

運用上は、例外を握りつぶすのではなく、どこで列挙しているか(責務)を意識して例外処理を設計するのが重要です。

リソース管理:列挙が中断されたときに片付くかを理解する

ファイルやDB接続などのリソースを使う場合、yieldの中で開いたものがいつ閉じられるかが重要になります。基本的に、列挙が終了・中断されると Dispose() が呼ばれ、finally が走ります(foreachは内部でDisposeを呼びます)。

ただし、呼び出し側が手動で列挙子を扱うのに Dispose を忘れると、片付けが遅れたり漏れたりします。リソースを扱う列挙は、利用側も含めて設計・レビュー対象にしておくのが無難です。

IEnumerable<string> ReadLinesLazy(string path)
{
    using var reader = new StreamReader(path);
    string? line;
    while ((line = reader.ReadLine()) != null)
    {
        yield return line;
    }
    // foreach を抜けると Dispose が呼ばれて reader も閉じられる
}

この形はシンプルですが、「列挙されなければファイルを開かない」「途中で止めればそこで読むのを止める」という、yieldらしい特性が出ます。

try/catchyield の制約を知っておく

yield return には文法上の制約があります。特に、例外処理を濫用しているコードでは詰まりがちです。

  • yield returncatch ブロック内では使えません
  • yield returnfinally ブロック内でも使えません
  • yield return を含む trycatch を付ける形は不可(一方で try/finally は条件付きで可能)

「取得中に起きた例外を要素として流したい」などの要件がある場合は、yieldの中で無理に例外を握るより、結果型(例:Result<T>のような成功/失敗を表す型)を使う方が設計として綺麗になることが多いです。

スレッド安全ではない:同じ列挙子を同時に回さない

yieldで作られた列挙子は、内部に状態を持ちます。1つの列挙子を複数スレッドで同時に進めるのは危険です(設計としても避けるべきです)。

  • OK:同じIEnumerableから、別々に列挙子を作って、それぞれ別スレッドで列挙(ただしソースがスレッド安全である必要)
  • NG:同じ列挙子(IEnumerator)を共有して同時にMoveNext

LINQ と yield:遅延実行どうしを重ねるときの実務ポイント

LINQも多くの演算子(Select / Where / Take など)は遅延実行です。つまり、yieldで返したシーケンスにLINQを重ねると、「遅延の上に遅延が乗る」状態になります。

この構成自体は強力ですが、次を意識すると事故が減ります。

  • 副作用(ログ出力、DB更新など)を遅延列挙に入れない(必要なら責務分離)
  • 同じクエリを複数回列挙しない(必要なら ToList() で固定)
  • 列挙回数を意図的にコントロールする(レビューで確認)

「ここで一度確定させる」境界を作ると設計が安定する

たとえば、重いI/Oの部分は一度だけ実行して結果を固定し、その後の画面表示・並べ替え・フィルタはメモリ上でやる、といった境界を作ると、パフォーマンスと理解しやすさのバランスが取りやすいです。

「Taskっぽくしたい」「非同期で流したい」場合:IAsyncEnumerable<T> を検討する

質問でよく出るのが「yieldで並列っぽく見えるけど、Taskみたいにできる?」という点です。結論としては、並列化は別物ですが、非同期に“流す”ことはできます。

そのために用意されているのが IAsyncEnumerable<T>(C# 8 以降)です。これは await foreach で列挙でき、I/O待ちを挟みながら要素を順次返せます。

public static async IAsyncEnumerable<string> ReadLinesAsync(string path)
{
    using var reader = new StreamReader(path);
    while (!reader.EndOfStream)
    {
        var line = await reader.ReadLineAsync();
        if (line is null) yield break;
        yield return line; // 非同期に1行ずつ返せる
    }
}

// 利用側
await foreach (var line in ReadLinesAsync("big.txt"))
{
    Console.WriteLine(line);
}

ここで重要なのは、IAsyncEnumerable非同期(await)にはなりますが、やはり並列(同時実行)とは違うという点です。並列にしたいなら、別途戦略が必要です。

並列化したい場合の代表的な選択肢(目的別)

やりたいこと代表的な手段注意点
CPU処理を複数コアで並列Parallel.ForEach / Parallel.ForEachAsync / PLINQ順序保証、例外、スレッド安全性、粒度の調整が必要
I/Oを並行して待つTask.WhenAll(+同時実行数の制限)投げすぎるとサーバーやネットワークが詰まる
逐次でいいが“固まり”を避けたいIAsyncEnumerable + await foreachあくまで逐次。UI更新はスレッド切替も考慮

yieldは「列挙をきれいに書く」道具であり、「並列化」や「非同期化」そのものではない、という整理ができると迷いが減ります。

Unityのコルーチンでの yield return と混同しない

特にゲーム開発(Unity)経験がある場合、yield return に対して「待機」「フレームをまたぐ」「疑似的な並行処理」というイメージが強いかもしれません。

Unityのコルーチンは、IEnumeratorの仕組みをエンジン側が解釈して「次のフレームで再開」などのスケジューリングをしているため、見た目がTask的・非同期的に見えます。しかし、これも多くのケースで別スレッドで並列実行しているわけではありません(実際の並列化はJob Systemやスレッド・Taskなど別の仕組みが必要です)。

同じyield returnという文法でも、「誰が列挙子を回しているか」(.NETのforeachか、Unityエンジンか)で体験が変わる点は、誤解を生みやすいポイントです。

実戦での使い分け:迷ったときの判断基準

最後に、実務での「迷いどころ」を整理します。設計レビューや実装判断にそのまま使えるチェックリストです。

yield を選ぶと良い条件

  • 途中で打ち切れる可能性が高い(検索、フィルタ、上位N件)
  • 全件をメモリに載せたくない(巨大ファイル、ログ、ストリーム)
  • 結果を“流しながら”処理したい(パイプライン処理)
  • コードをシンプルに保ちたい(列挙子の自前実装を避けたい)

ToList() / ToArray() で具体化した方が良い条件

  • 同じ結果を複数回使う(再列挙で重い処理が走るのを避けたい)
  • 列挙のたびに結果が変わると困る(時刻、乱数、外部状態)
  • 例外やI/Oのタイミングを「この行で確定させたい」
  • 後続処理でランダムアクセスが必要(インデックス参照、並べ替えの繰り返し)

よくある質問

yield break は何に使う?

yield break は列挙を終了させるためのキーワードです。条件に応じて早期終了したいときに使います。

public static IEnumerable<int> UntilNegative(IEnumerable<int> source)
{
    foreach (var x in source)
    {
        if (x < 0) yield break;
        yield return x;
    }
}

yield のメソッドは呼び出した瞬間に実行される?

多くの場合、実行されません。列挙が開始され、MoveNext()が呼ばれたタイミングで実行されます。つまり「メソッドを呼ぶ」と「列挙する」は別物です。

yieldで返したものをデバッグすると挙動が分かりにくい…

状態マシンに変換されるため、ステップ実行時に「見た目通りに進まない」ことがあります。対策としては、以下が有効です。

  • ログは「列挙が開始された瞬間」と「要素を返した瞬間」を分けて書く
  • 問題切り分けでは一時的に ToList() して実行タイミングを固定する
  • “境界”を設ける(I/O部は具体化してから次へ)

まとめ:yield を正しく理解すると、速さより「無駄の削減」と「設計の自由度」が手に入る

yieldは並列化の道具ではなく、IEnumerable<T> を遅延実行で1件ずつ返すための仕組みです。速くなるかどうかはケース次第ですが、途中で打ち切れる場面や巨大データのストリーミング処理では、総仕事量・メモリ・応答性の面で大きなメリットが出ます。

一方で、「再列挙で再実行」「例外のタイミング」「リソース管理」などの落とし穴もあります。yieldを“便利なショートカット”として使うのではなく、遅延実行という性質を前提に設計すると、堅牢で伸びるコードになります。

この記事を書いた人

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

コメント

コメントする

目次