すべての foreach ループとすべての LINQ クエリは、1つの小さなインターフェース IEnumerable<T> を通して動いています。これを理解すると、LINQが遅延評価される理由、一部のシーケンスが1回しか読めない理由、そして yield return を使うと普通のループで独自のシーケンスを書ける仕組みがわかります。
IEnumerableとは何か
IEnumerable<T> のメソッドは GetEnumerator() の1つだけです。それが返す列挙子は、MoveNext()(次に進み、要素があるかを報告する)と Current(要素)を持ちます。foreach は、その列挙子を動かすための省略形です。
出力:
red green blue
red green blue
列挙子は最初の要素の前から始まるので、最初の MoveNext() で最初の要素に移ります。MoveNext() が false を返すとシーケンスは終わりです。using ブロックが列挙子を破棄し、これは開いたファイルなどのリソースを持つイテレーターで重要になります。
IEnumerable、ICollection、Listの比較
コレクションのインターフェースは段階を成していて、上の段ほど多くを約束します。
| 型 | 加わるもの | 典型的な用途 |
|---|---|---|
IEnumerable<T> | foreach のみ | ループするだけの引数、LINQの結果、遅延評価されるシーケンス |
IReadOnlyCollection<T> | Count | 呼び出し側に変更させない戻り値 |
ICollection<T> | Count、Add、Remove、Contains | 任意の種類のコレクションに追加するコード |
IReadOnlyList<T> | インデックスによるアクセス [i] | 位置を持つ読み取り専用の結果 |
IList<T> | インデックスによるアクセス、Insert、RemoveAt | 位置で編集するコード |
List<T> | これらすべてを実装する具体的なクラス | 格納 |
入力をループするだけのメソッドは IEnumerable<T> を受け取るべきです。そうすれば、呼び出し側は配列、リスト、セット、LINQクエリを変換せずに渡せます。データを返すメソッドは、通常は List<T> や IReadOnlyList<T> のように「これはすでにメモリにある」ことを表すものを返すべきです。IEnumerable<T> の戻り値は、読まれるたびに処理を行うクエリかもしれないからです。
出力:
30
15
4.0
30
yield returnでイテレーターを書く
独自のシーケンスを生成するには、IEnumerable<T> を返すメソッドを書き、各要素に yield return を使います。コンパイラがメソッドを、列挙子を実装するクラスに変換してくれます。
出力:
0, 2, 4, 6, 8, 10
Mon Tue Wed Thu Fri
EvenNumbers のどこにもリストはありません。yield return のたびに値が1つ呼び出し側に渡されてメソッドが一時停止し、呼び出し側が次の値を求めるまでローカル変数(ここでは i)が保たれます。
遅延評価:実行の順序を見る
メソッドが一時停止するので、メソッドの本体と呼び出し側のループが交互に実行されます。両方から表示すると、その入れ替わりがわかります。
出力:
Called Numbers(), nothing printed yet
start
yielding 1
Loop got 1
resumed after 1
yielding 2
Loop got 2
resumed after 2
yielding 3
Loop got 3
resumed after 3
end
Numbers() を呼んでも「start」は表示されませんでした。列挙可能なオブジェクトが作られただけです。本体は foreach の中の最初の MoveNext() で始まり、最初の yield return まで実行されて止まりました。その後の各ステップは yield の次の行から再開しました。LINQの Where と Select も同じように動きます(ランタイムは速度のために手書きの列挙子クラスで実装していますが、効果は同じです)。これがLINQのクエリが遅延実行される理由です。
yield break
yield break はシーケンスを早めに終わらせます。イテレーター版の return です。
出力:
Subject: Invoice
From: shop@example.com
1つのメソッドで return value; と yield return を混ぜることはできません。メソッドに yield が含まれると、それはイテレーターになり、早めに抜ける方法は yield break だけです。
無限シーケンス
要求された要素しか計算されないので、イテレーターは終わりのないシーケンスを表現できます。どれだけ取るかは使う側が決めます。
出力:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
1597
2026-09-28 Mon
2026-10-05 Mon
2026-10-12 Mon
ここで while (true) が安全なのは、Take と First が必要なものを得た後は要求をやめるからです。一方、無限シーケンスに ToList() や Count() を呼ぶと決して戻らず、それに対する OrderBy を列挙しても同じです。並べ替えには先にすべての要素が必要だからです。
遅延評価されるシーケンスの落とし穴
引数のチェックが遅れて実行される。 イテレーターの本体は、先頭の検証も含めて丸ごと遅延されます。直し方はメソッドを分けることです。通常のメソッドが引数をチェックし、privateなイテレーターの結果を返します。
出力:
LazyRange(-1) returned without an error
...and threw only when enumerated
EagerRange(-1) threw at the call
列挙のたびにメソッドが再実行される。 イテレーターは何もキャッシュしません。Numbers() を2回ループすると「start」が2回表示され、ファイルが2回読まれ、Webサービスが2回呼ばれます。IDEはこれを「possible multiple enumeration of IEnumerable」として警告します。要素を複数回使うなら、ToList() を1回呼びます。
使う側が止めたときに finally が実行される。 呼び出し側が foreach から早めに抜けると、列挙子が破棄され、イテレーターの中の finally ブロックが実行されます。ファイルを読むイテレーターは、この仕組みでファイルを閉じます。
出力:
open sensor
10
20
30
close sensor
done
コンパイラがイテレーターに課すルールがいくつかあります。yield return は、catch を持つ try ブロックの中(try/finally なら問題ありません)、ラムダや匿名メソッドの中、ref や out の引数を持つメソッドの中には書けません。
非同期ストリーム
要素が非同期に届くシーケンス(APIからのページ、データベースからの行)には、C# 8以降(.NET Core 3.0以降)で、IAsyncEnumerable<T> を通してイテレーターと await を組み合わせられます。
static async IAsyncEnumerable<string> FetchPages()
{
for (int page = 1; page <= 3; page++)
{
await Task.Delay(100); // simulate a network call
yield return $"page {page}";
}
}
await foreach (string p in FetchPages())
{
Console.WriteLine(p);
}
使う側は await foreach を使い、各要素は生成されるたびにawaitされます。
よくある間違い
- クエリから
IEnumerable<T>を返し、何度も読む。 読むたびに再実行されます。代わりにList<T>を返します。 - イテレーターの中で引数を検証する。 チェックが遅延されます。ラッパーとイテレーターに分けます。
- 無限イテレーターに
Count()やToList()を呼ぶ。 終わりません。先にTakeを使います。 IEnumerable<T>の引数がCountや[i]を持つと思う。 LINQのCount()(列挙することがあります)を使うか、IReadOnlyList<T>を受け取ります。
よくある質問
C#のIEnumerableとは何ですか?
IEnumerable<T> は、foreach でループできるもののためのインターフェースです。メソッドは GetEnumerator() の1つだけで、MoveNext() と Current を持つ IEnumerator<T> を返します。配列、リスト、辞書、セット、LINQクエリ、イテレーターメソッドはすべてこれを実装しています。
C#のIEnumerableとListの違いは何ですか?
IEnumerable<T> が約束するのは反復できることだけで、Count プロパティも、インデクサーも、Add もなく、ループするたびに遅延計算されることもあります。List<T> は、それらすべてを備えた、メモリに格納される具体的なコレクションです。ループするだけなら引数には IEnumerable<T> を受け取り、呼び出し側がインデックスや要素数を必要とするなら List<T> か IReadOnlyList<T> を返します。
C#のyield returnは何をするものですか?
IEnumerable<T> を返すメソッドの中の yield return value; は、呼び出し側に要素を1つ渡し、その地点でメソッドを一時停止します。次に呼び出し側が要素を求めると、メソッドは次の行から再開します。コンパイラがメソッドをステートマシンに書き換えるので、ループを書くだけで遅延評価されるシーケンスが得られます。
C#のyield breakとは何ですか?
yield break; はシーケンスを終わらせます。呼び出し側の foreach は、すでに生成された要素の後で通常どおり終了します。イテレーターにおける return に当たるもので、上限に達したときや入力が空のときなど、早めに止めるために使います。
イテレーターメソッドを呼び出しても例外が投げられないのはなぜですか?
イテレーターメソッドの本体はシーケンスが列挙されるまで実行されないので、先頭の引数チェックは呼び出し時ではなく最初の MoveNext で実行されます。すぐに検証するには、チェックを通常のpublicなメソッドに置き、そこから yield 文を持つprivateなイテレーターメソッドを呼び出します。