6

だから、この質問はちょうどSOで尋ねられました:

「無限の」IEnumerableを処理する方法は?

私のサンプルコード:

public static void Main(string[] args)
{
    foreach (var item in Numbers().Take(10))
        Console.WriteLine(item);
    Console.ReadKey();
}

public static IEnumerable<int> Numbers()
{
    int x = 0;
    while (true)
        yield return x++;
}

誰かがこれが遅延評価される理由を説明できますか?私はReflectorでこのコードを調べましたが、始めたときよりも混乱しています。

リフレクター出力:

public static IEnumerable<int> Numbers()
{
    return new <Numbers>d__0(-2);
}

数値メソッドの場合、その式の新しい型を生成したように見えます。

[DebuggerHidden]
public <Numbers>d__0(int <>1__state)
{
    this.<>1__state = <>1__state;
    this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;
}

これは私には意味がありません。そのコードをまとめて自分で実行するまでは、無限ループだと思っていたでしょう。

編集:これで、.Take()は、列挙が「終了」したことをforeachに伝えることができますが、実際には終了していないのに、Take()にチェーンする前にNumbers()全体を呼び出すべきではないことを理解しました。 ?テイクの結果は、実際に列挙されているものですよね?しかし、Numbersが完全に評価されていない場合、Takeはどのように実行されますか?

EDIT2:では、これは「yield」キーワードによって強制される特定のコンパイラトリックですか?

4

3 に答える 3

2

これは次のことと関係があります。

  • 特定のメソッドが呼び出されたときにiEnumerableが行うこと
  • 列挙の性質とYieldステートメント

あらゆる種類のIEnumerableを列挙すると、クラスは次のアイテムを提供します。すべてのアイテムに影響を与えるわけではなく、次のアイテムを提供するだけです。それはそのアイテムがどうなるかを決定します。(たとえば、一部のコレクションは順序付けられていますが、そうでないものもあります。特定の順序を保証しないものもありますが、常に同じ順序で返されるようです。)

IEnumerable拡張メソッドTake()は10回列挙し、最初の10個のアイテムを取得します。あなたはそうすることができTake(100000000)ました、そしてそれはあなたにたくさんの数を与えるでしょう。しかし、あなたはただやっていTake(10)ます。Numbers()次のアイテムを要求するだけです。。。10回。

それらの10個のアイテムのそれぞれはNumbers、次のアイテムを与えます。その方法を理解するには、Yieldステートメントを読む必要があります。これは、より複雑なもの の構文糖衣です。収量は非常に強力です。(私はVB開発者であり、まだ持っていないことに非常に悩まされています。)これは機能ではありません。これは、特定の制限があるキーワードです。また、列挙子の定義が他の方法よりもはるかに簡単になります。

他のIEnumerable拡張メソッドは、常にすべてのアイテムを反復処理します。.AsListを呼び出すと、それが爆発します。それを使用すると、ほとんどのLINQクエリはそれを爆破します。

于 2010-04-29T19:23:05.553 に答える
1

これが無限ループではない理由は、LinqのTake(10)呼び出しの使用に従って10回しか列挙していないためです。ここで、次のようなコードを記述した場合:

foreach (var item in Numbers())
{
}

列挙子は常に新しい値を返すため、これは無限ループになります。C#コンパイラはこのコードを受け取り、ステートマシンに変換します。列挙子に実行を中断するためのガード句がない場合、呼び出し元はサンプルで実行を中断する必要があります。

コードが怠惰である理由は、コードが機能する理由でもあります。基本的に、Takeは最初のアイテムを返し、次にアプリケーションが消費し、10個のアイテムを取得するまで別のアイテムを受け取ります。

編集

これは実際にはテイクの追加とは何の関係もありません。これらはイテレータと呼ばれます。C#コンパイラは、コードに対して複雑な変換を実行し、メソッドから列挙子を作成します。私はそれを読むことをお勧めしますが、基本的に(そしてこれは100%正確ではないかもしれません)、あなたのコードはあなたがステートマシンを初期化することとして想像できるNumbersメソッドに入ります。

コードがyieldリターンに達すると、基本的に、Numbers()の実行を停止してこの結果を返し、次のアイテムを要求すると、yieldリターンの次の行で実行を再開します。

Erik Lippertは、イテレータのその他の側面に関するすばらしいシリーズを持っています

于 2010-04-29T19:22:36.943 に答える
0

基本的に、Numbers()関数は列挙子を作成します。
foreachは、各反復で、列挙子が最後に到達したかどうかをチェックし、到達しなかった場合は続行します。あなたのプラティキュラー列挙子は決して終わらないでしょう、しかしそれは問題ではありません。これは怠惰に評価されています。
列挙子は結果を「ライブ」で生成します。
つまり、そこに.Take(3)と書くと、ループは3回しか実行されません。列挙子にはまだいくつかの項目が「残っています」が、現時点ではメソッドがそれらを必要としないため、それらは生成されません。
関数が示すように0から無限大までのすべての数値を生成し、それらを一度に返す場合、そのうちの10個のみを使用するこのプログラムははるかに遅くなります。これが遅延評価の利点です。使用されなかったものが計算されることはありません。

于 2010-04-29T19:23:46.803 に答える