64 votes

Pourquoi cette méthode dans une boucle infinie?

Un de mes collègues est venu à moi avec une question à propos de cette méthode qui se traduit dans une boucle infinie. Le code est un peu trop impliqué à poster ici, mais en gros, le problème se résume à ceci:

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

Ce doit (vous ne le pensez) juste être une voie très inefficace pour créer une copie d'une liste. Je l'ai appelé avec:

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

Le résultat est une boucle infinie. Étrange.

Je pense que le fait de modifier le paramètre est, stylistiquement une mauvaise chose, j'ai donc modifié le code légèrement:

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

Qui ont travaillé. Qui est, le programme terminé; aucune exception.

Plusieurs expériences ont montré que cela fonctionne aussi:

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

Comme le fait un simple

return items.Select(item => .....);

Curieux.

Il est clair que le problème a à voir avec la reprogrammation des paramètres, mais seulement si l'évaluation est reportée au-delà de cette déclaration. Si j'ajoute l' ToList() il fonctionne.

J'ai une vague idée de ce qui va mal. Il ressemble à l' Select est une itération sur sa propre production. C'est un peu étrange en elle-même, parce que en général, une IEnumerable lèvera si la collection, il est l'itération des changements.

Ce que je ne comprends pas, parce que je ne suis pas intimement familier avec le fonctionnement interne de la façon dont ça fonctionne, c'est pourquoi la ré-affectation du paramètre sont les causes de cette boucle infinie.

Est-il quelqu'un avec plus de connaissances sur le fonctionnement interne qui serait prêt à m'expliquer pourquoi la boucle infinie se produit ici?

64voto

dasblinkenlight Points 264350

La clé pour répondre à cette exécution différée. Lorsque vous faites cela

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

vous n'avez pas à itérer l' items tableau passé à la méthode. Au lieu de cela, vous lui affectez un nouveau IEnumerable<int>, qui renvoie à lui-même de retour, et commence à l'itération seulement lorsque l'appelant commence l'énumération des résultats.

C'est pourquoi tous vos autres correctifs ont abordé le problème: tous vous avez besoin à faire est d'arrêter de nourrir IEnumerable<int> retour à lui-même:

  • À l'aide de var foo sauts de l'auto-référence en utilisant une autre variable,
  • À l'aide de return items.Select... sauts de l'auto-référence en n'utilisant pas de variables intermédiaires à tous,
  • À l'aide de ToList() sauts de l'auto-référence en évitant l'exécution différée: par le temps items est attribué à nouveau, vieux - items a été itéré, si vous vous retrouvez avec une plaine de mémoire- List<int>.

Mais si c'est de la nourrir de lui-même, comment fait-il quoi que ce soit?

C'est vrai, il n'a pas obtenir quoi que ce soit! Le moment où vous essayez de l'itération items et lui demander pour le premier élément, le report de la séquence de demande à la séquence de la fed pour le premier élément, ce qui signifie que la séquence est de demander lui-même pour le premier élément de processus. À ce stade, c'est les tortues tout le chemin vers le bas, parce que, pour retourner le premier élément du processus de la séquence doit d'abord obtenir le premier élément de processus à partir de lui-même.

20voto

D Stanley Points 54768

Il ressemble à la Select est une itération sur sa propre production

Vous êtes correct. Vous êtes de retour d'une requête qui effectue une itération sur lui-même.

La clé est que vous référencez items au sein de la lambda. L' items de référence n'est pas résolu ("fermés") jusqu'à ce que la requête effectue une itération, à quel point items maintenant référence à la requête au lieu de la source de la collection. C'est là où l'auto-référence produit.

L'image d'un jeu de cartes avec un signe en face de lui étiquetés items. Maintenant l'image d'un homme debout à côté de la pioche de cartes dont la mission est d'itérer la collection appelée items. Mais ensuite, vous déplacez le signe de la platine par rapport à l' homme. Lorsque vous demandez à l'homme pour la première "le point" - il a l'air pour la collecte marqué "éléments" - qui est maintenant lui! Donc, il demande lui-même pour le premier élément, qui est l'endroit où la référence circulaire se produit.

Lorsque vous affectez le résultat d'une nouvelle variable, vous avez alors une requête qui effectue une itération sur une différents de collecte, et donc ne résulte pas dans une boucle infinie.

Lorsque vous appelez ToList, vous hydrater la requête pour une nouvelle collection et aussi ne pas obtenir une boucle infinie.

D'autres choses qui enfreignent la circulaire de référence:

  • Hydratant éléments à l'intérieur de la lambda en appelant ToList
  • Attribution d' items d'une autre variable et de référencement que dans le lambda.

5voto

Jim Mischel Points 68586

Après avoir étudié les deux réponses données et de fouiller un peu, je suis venu avec un petit programme qui illustre le mieux le problème.

    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;
    }

Si vous l'appelez avec:

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

Vous obtiendrez cette sortie à plusieurs reprises jusqu'à ce que vous obtenez finalement StackOverflowException.

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

Ce que cela montre, c'est exactement ce que dasblinkenlight clair dans sa mise à jour réponse: la requête dans une boucle infinie en essayant d'obtenir le premier élément.

Écrivons GoNuts d'une manière légèrement différente:

    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;
    }

Si vous exécutez, il réussit. Pourquoi? Parce que dans ce cas, il est clair que l'appel à GetFirst est de passer une référence à l'origine des éléments qui ont été transmis à la méthode. Dans le premier cas, GetFirst est de passer une référence à la nouvelle - items de la collection, qui n'a pas encore été réalisé. À son tour, GetFirst , dit, "Hey, j'ai besoin d'énumérer cette collection." Et ainsi commence le premier appel récursif qui mène éventuellement à la StackOverflowException.

Fait intéressant, j'avais raison et tort quand j'ai dit qu'il était en train de consommer sa propre production. L' Select consomme de l'entrée d'origine, que je m'attends. L' First est d'essayer de consommer de la sortie.

Beaucoup de leçons à apprendre ici. Pour moi, le plus important est "ne pas modifier la valeur des paramètres d'entrée."

Grâce à dasblinkenlight, D Stanley, et Lucas Trzesniewski pour leur aide.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X