yield
キーワードは、あなたが作成することができますIEnumerable<T>
上のフォームで反復子ブロック。この反復子ブロックは遅延実行をサポートしており、概念に慣れていない場合は、ほとんど魔法のように見えるかもしれません。ただし、結局のところ、奇妙なトリックなしで実行されるのはコードだけです。
イテレータブロックは、コンパイラが列挙型の列挙がどこまで進んだかを追跡するステートマシンを生成する構文シュガーとして説明できます。enumerableを列挙するには、foreach
ループを使用することがよくあります。ただし、foreach
ループも構文上の砂糖です。したがって、実際のコードから2つの抽象概念が削除されているため、最初はすべてがどのように連携しているかを理解するのが難しい場合があります。
非常に単純なイテレーターブロックがあると仮定します。
IEnumerable<int> IteratorBlock()
{
Console.WriteLine("Begin");
yield return 1;
Console.WriteLine("After 1");
yield return 2;
Console.WriteLine("After 2");
yield return 42;
Console.WriteLine("End");
}
実際のイテレーターブロックには、多くの場合、条件とループがありますが、条件をチェックしてループを展開すると、最終的に次のようになります。 yield
ステートメントが他のコードとインターリーブます。
イテレータブロックを列挙するには、foreach
ループを使用します。
foreach (var i in IteratorBlock())
Console.WriteLine(i);
ここに出力があります(ここには驚きはありません):
ベギン
1
1の後
2
2の後
42
終わり
上記のようforeach
に構文糖です:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
これを解くために、抽象化を削除したシーケンス図を作成しました:
コンパイラーによって生成された状態マシンも列挙子を実装しますが、図をより明確にするために、それらを個別のインスタンスとして示しました。(状態マシンが別のスレッドから列挙される場合、実際には個別のインスタンスを取得しますが、その詳細はここでは重要ではありません。)
イテレータブロックを呼び出すたびに、ステートマシンの新しいインスタンスが作成されます。ただし、イテレーターブロック内のコードはenumerator.MoveNext()
、初めて実行されるまで実行されません。これが遅延実行の仕組みです。これは(むしろばかげた)例です:
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
この時点では、イテレーターは実行されていません。このWhere
句は、によって返されたIEnumerable<T>
をラップする新しいものを作成しますが、この列挙型はまだ列挙されていません。これは、ループを実行すると発生します。IEnumerable<T>
IteratorBlock
foreach
foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber);
enumerableを2回列挙すると、ステートマシンの新しいインスタンスが毎回作成され、イテレーターブロックが同じコードを2回実行します。
以下のようなLINQの方法ということに注意してくださいToList()
、ToArray()
、First()
、Count()
などが使用するforeach
列挙を列挙するためにループを。たとえばToList()
、enumerableのすべての要素を列挙し、それらをリストに格納します。これで、リストにアクセスして、イテレーターブロックを再度実行しなくても、列挙可能なすべての要素を取得できます。のようなメソッドを使用する場合、CPUを使用して列挙可能な要素を複数回生成することと、列挙の要素を格納してそれらに複数回アクセスするためのメモリとの間にはトレードオフがありますToList()
。