1

与えられた開始点からランダムな数字のリストを作成するように設計されたプログラムを書きました。それは簡単で汚いものでしたが、それで遊んでいると、よくわからない興味深い効果が見つかりました。

void Main()
{
    List<int> foo = new List<int>(){1,2,3};
    IEnumerable<int> bar = GetNumbers(foo);
    for (int i = 1; i < 3; i++)
    {
        foo = new List<int>(){1,2,3};
        var wibble = GetNumbers(foo);
        bar = bar.Concat(wibble);
    }
    Iterate(bar);
    Iterate(bar);
}

public void Iterate(IEnumerable<int> numbers)
{
    Console.WriteLine("iterating");
    foreach(int number in numbers)
    {
        Console.WriteLine(number);
    }
}

public IEnumerable<int> GetNumbers(List<int> input)
{
    //This function originally did more but this is a cutdown version for testing.
    while (input.Count>0)
    {
        int returnvalue = input[0];
        input.Remove(input[0]);
        yield return returnvalue;
    }
}

これを実行した結果は次のとおりです。

iterating
1
2
3
1
2
3
1
2
3
iterating

つまり、bar空になった直後に 2 回目の繰り返しを行います。

これは、リストを生成するために使用されているリストを初めて空にすることを反復し、その後、空になった同じリストを使用して反復するという事実と関係があると思います。

私の混乱は、なぜこれが起こっているのですか?IEnumerables を列挙するたびに既定の状態から開始されないのはなぜですか? 誰かが私がここで何をしているのか正確に説明できますか?

.ToList()明確にするために、呼び出しに を追加することでこの問題を解決できることを知っていGetNumbers()ます。これにより、結果の即時評価と保存が強制されます。

4

3 に答える 3

6

イテレータは初期状態から開始します。ただし、読み取り元のリストを変更し、リストがクリアされると、イテレーターは何もする必要がなくなります。基本的には検討

var list = new List<int> { 1, 2, 3 };
var enumerable = list.Where(i => i != 2);
foreach (var item in enumerable)
    Console.WriteLine(item);
list.Clear();
foreach (var item in enumerable)
    Console.WriteLine(item);

enumerableによって変更されませんlist.Clear();が、それが与える結果は変更されます。

于 2012-08-23T16:12:49.050 に答える
3

あなたの観察は、このメイン メソッドの短いバージョンで再現できます。

void Main() 
{ 
    List<int> foo = new List<int>(){1,2,3}; 
    IEnumerable<int> bar = GetNumbers(foo); 
    Console.WriteLine(foo.Count); // prints 3
    Iterate(bar); 
    Console.WriteLine(foo.Count); // prints 0
    Iterate(bar); 
} 

何が起こるかは次のとおりです。

あなたが呼び出すとき、GetNumbersそれは実際には実行されていません。結果を反復処理する場合にのみ実行されます。とConsole.WriteLine(foo.Count);の呼び出しの間に挿入することで、これを確認できます。 への最初の呼び出しで、が実行され、foo が空になります。の 2 回目の呼び出しで,が再び実行されますが、今度は foo が空なので、返すものが残っていません。GetNumbersIterate
IterateGetNumbersIterateGetNumbers

于 2012-08-23T16:16:08.843 に答える
1

さて、怠惰な評価はあなたを襲ったものです。ご覧のとおり、yield returnスタイルメソッドを作成すると、呼び出し直後には実行されません。ただし、シーケンスを反復処理するとすぐに実行されます。

つまり、これは、の間にリストがクリアされるのではなく、の間GetNumbersにのみクリアされることを意味しIterateます。実際、関数全体は。のGetNumbers間にのみ実行されIterateます。

問題は、内部状態だけでなく外部IEnumersble状態にも依存するようにしたことです。その外側の状態がリストの内容です。foo

したがって、すべてのリストはIterate、初めて使用するまでいっぱいになります。(IEnumerableによって作成されたものGetNumbersはそれらへの参照を保持しているので、上書きするという事実fooは重要ではありません。)最初の間に3つすべてが空になりIterateます。次に、次の反復は同じ内部状態で開始されますが、外部状態が変更され、異なる結果が得られます。

私が気づきたいのは、突然変異と外部状態への依存は、関数型プログラミングスタイルでは一般的に眉をひそめているということです。LINQは実際には関数型プログラミングへの一歩なので、FPのルールに従うことをお勧めします。inputしたがって、内からアイテムを削除しない方がよいでしょうGetNumbers

于 2012-08-23T16:16:42.673 に答える