web-dev-qa-db-ja.com

この方法で無限ループが発生するのはなぜですか?

同僚の1人が、無限ループを引き起こすこの方法について質問をしました。実際のコードは少し複雑すぎてここに投稿することはできませんが、基本的に問題はこれに要約されます。

_private IEnumerable<int> GoNuts(IEnumerable<int> items)
{
    items = items.Select(item => items.First(i => i == item));
    return items;
}
_

これはすべき(あなたが思うでしょう)は、リストのコピーを作成するための非常に非効率的な方法です。私はそれを次のように呼び出しました:

_var foo = GoNuts(new[]{1,2,3,4,5,6});
_

結果は無限ループになります。奇妙な。

パラメータを変更することは、文体的には悪いことだと思うので、コードを少し変更しました。

_var foo = items.Select(item => items.First(i => i == item));
return foo;
_

うまくいきました。つまり、プログラムが完了しました。例外なし。

より多くの実験がこれも機能することを示しました:

_items = items.Select(item => items.First(i => i == item)).ToList();
return items;
_

単純なように

_return items.Select(item => .....);
_

奇妙な。

問題がパラメーターの再割り当てに関係していることは明らかですが、評価がそのステートメントを超えて延期された場合のみです。 ToList()を追加すると機能します。

何が問題なのか、一般的で漠然とした考えがあります。 Selectが独自の出力を繰り返し処理しているようです。通常、IEnumerableは、反復するコレクションが変更された場合にスローするため、それ自体は少し奇妙です。

私が理解していないのは、これがどのように機能するかについての内部に精通していないため、パラメーターを再割り当てするとこの無限ループが発生する理由です。

無限ループがここで発生する理由を説明しようとする内部の知識を持つ誰かがいますか?

64
Jim Mischel

これに答える鍵は、遅延実行です。あなたがこれをするとき

_items = items.Select(item => items.First(i => i == item));
_

メソッドに渡されたitems配列をしない繰り返します代わりに、新しい__IEnumerable<int>_を割り当て、それ自体を参照し直し、呼び出し元が結果の列挙を開始したときにのみ反復を開始します。

他のすべての修正で問題が解決されたのはそのためです。必要なことは、_IEnumerable<int>_へのフィードを停止することだけです。

  • _var foo_を使用すると、別の変数を使用して自己参照が壊れます。
  • _return items.Select..._を使用すると、中間変数をまったく使用しないため、自己参照が無効になります。
  • ToList()を使用すると、遅延実行を回避することで自己参照が無効になります。itemsが再割り当てされるまでに、古いitemsが繰り返されているため、最終的にプレーンなメモリ_List<int>_になります。

しかし、それがそれ自体を食べているなら、どうやってそれは何かを得るのですか?

そうです、何も得られません! itemsを反復して最初のアイテムを要求すると、遅延シーケンスは最初のアイテムの処理を要求されます。つまり、シーケンスは最初のアイテムの処理を要求しています。この時点で、それは カメがずっと下にある です。これは、最初のアイテムを返してシーケンスを処理するために、最初に最初のアイテムをそれ自体から取得する必要があるためです。

64
dasblinkenlight

Selectが独自の出力を繰り返し処理しているようです

あなたは正しいです。あなたは自分自身を繰り返すqueryを返しています。

重要なのは、itemsラムダ内を参照することです。 items参照は、クエリが反復するまで解決(「クローズ」)されません。その時点で、itemsはソースコレクションではなくクエリを参照するようになります。 That's自己参照が発生する場所。

その前にitemsというラベルの付いた看板があるカードの山を想像してください。次に、itemsと呼ばれるコレクションを繰り返すことを割り当てのカードのデッキの横に立っている男性を想像してください。しかし、その後、デッキからデッキをmanに移動します。男に最初の「アイテム」を要求すると、彼は「アイテム」とマークされたコレクションを探します。それで彼は、循環参照が発生する最初の項目を尋ねます。

結果をnew変数に割り当てると、クエリはdifferentコレクションを反復処理するため、無限ループにはなりません。

ToListを呼び出すと、クエリが新しいコレクションにハイドレートされ、無限ループも発生しません。

循環参照を壊す他のこと:

  • アイテムのハイドレートラムダ内を呼び出すことでToList
  • itemsを別の変数に割り当て、ラムダ内でthatを参照します。
20
D Stanley

与えられた2つの答えを調べて少し調べた後、問題をよりよく説明する小さなプログラムを思いつきました。

    private int GetFirst(IEnumerable<int> items, int foo)
    {
        Console.WriteLine("GetFirst {0}", foo);
        var rslt = items.First(i => i == foo);
        Console.WriteLine("GetFirst returns {0}", rslt);
        return rslt;
    }

    private IEnumerable<int> GoNuts(IEnumerable<int> items)
    {
        items = items.Select(item =>
        {
            Console.WriteLine("Select item = {0}", item);
            return GetFirst(items, item);
        });
        return items;
    }

それを次のように呼び出すと:

var newList = GoNuts(new[]{1, 2, 3, 4, 5, 6});

最終的にStackOverflowExceptionを取得するまで、この出力が繰り返し表示されます。

Select item = 1
GetFirst 1
Select item = 1
GetFirst 1
Select item = 1
GetFirst 1
...

これが示すのは、dasblinkenlightが彼の更新された回答で明らかにしたものです。クエリは、最初のアイテムを取得しようとする無限ループに入ります。

少し違う方法でGoNutsを書いてみましょう:

    private IEnumerable<int> GoNuts(IEnumerable<int> items)
    {
        var originalItems = items;
        items = items.Select(item =>
        {
            Console.WriteLine("Select item = {0}", item);
            return GetFirst(originalItems, item);
        });
        return items;
    }

それを実行すると、成功します。どうして?この場合、GetFirstへの呼び出しが、メソッドに渡された元のアイテムへの参照を渡していることが明らかであるためです。最初のケースでは、GetFirstnewitemsコレクションへの参照を渡していますが、まだ実現されていません。次に、GetFirstは、「ねえ、私はこのコレクションを列挙する必要があります」と言います。そして、最終的にStackOverflowExceptionにつながる最初の再帰呼び出しを開始します。

おもしろいことに、私は正しかったand間違っていた。 Selectは、予想どおり、元の入力を消費しています。 Firstは出力を消費しようとしています。

ここで学ぶべき多くの教訓。私にとって最も重要なのは、「入力パラメーターの値を変更しないこと」です。

Dasblinkenlight、D Stanley、およびLucas Trzesniewskiの協力に感謝します。

5
Jim Mischel