Un de mes collègues est venu me poser une question sur cette méthode qui se traduit par une boucle infinie. Le code réel est un peu trop compliqué à publier ici, mais essentiellement 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;
}
Cela devrait (on pourrait penser) est juste un moyen très inefficace de 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 la modification du paramètre est, stylistiquement, une mauvaise chose, j'ai donc légèrement changé le code:
var foo = items.Select(item => items.First(i => i == item));
return foo;
Ça a marché. Autrement dit, le programme est terminé; pas exception.
Plus d'expériences ont montré que cela fonctionne aussi:
items = items.Select(item => items.First(i => i == item)).ToList();
return items;
Tout comme un simple
return items.Select(item => .....);
Curieuse.
Il est clair que le problème est lié à la réaffectation du paramètre, mais uniquement si l'évaluation est différée au-delà de cette déclaration. Si j'ajoute la ToList()
ça marche.
J'ai une idée générale et vague de ce qui ne va pas. Il semble que le Select
itère sur sa propre sortie. C'est un peu étrange en soi, car généralement un IEnumerable
lancera si la collection qu'il itère change.
Ce que je ne comprends pas, parce que je ne connais pas intimement le fonctionnement de ce truc, c'est pourquoi la réaffectation du paramètre provoque cette boucle infinie.
Y a-t-il quelqu'un avec plus de connaissances des internes qui serait disposé à expliquer pourquoi la boucle infinie se produit ici?
La clé pour y répondre est l'exécution différée . Quand tu fais ça
items = items.Select(item => items.First(i => i == item));
vous ne pas itérer le tableau items
passé dans la méthode. Au lieu de cela, vous lui affectez un nouveau IEnumerable<int>
, Qui se réfère à lui-même et commence l'itération uniquement lorsque l'appelant commence à énumérer les résultats.
C'est pourquoi tous vos autres correctifs ont résolu le problème: tout ce que vous aviez à faire était d'arrêter de renvoyer IEnumerable<int>
À lui-même:
var foo
Rompt l'auto-référence en utilisant une variable différente,return items.Select...
Rompt l'auto-référence en n'utilisant pas du tout de variables intermédiaires,ToList()
rompt l'auto-référence en évitant une exécution différée: au moment où items
est réaffecté, l'ancien items
a été réitéré, vous vous retrouvez donc avec une simple en mémoire List<int>
.Mais s'il se nourrit de lui-même, comment obtient-il quelque chose?
C'est vrai, ça n'obtient rien! Au moment où vous essayez d'itérer items
et lui demandez le premier élément, la séquence différée demande à la séquence qui lui est envoyée le premier élément à traiter, ce qui signifie que la séquence se demande le premier élément à traiter. À ce stade, c'est tortues tout le long , car pour renvoyer le premier élément à traiter, la séquence doit d'abord obtenir le premier élément à traiter de lui-même.
Il semble que le Select itère sur sa propre sortie
Vous avez raison. Vous retournez un requête qui itère sur lui-même.
La clé est que vous faites référence à items
dans le lambda. La référence items
n'est pas résolue ("fermée") tant que la requête n'est pas répétée, point auquel items
fait désormais référence à la requête au lieu de la collection source. C'est où se produit l'auto-référence.
Imaginez un jeu de cartes avec un signe devant lui intitulé items
. Imaginez maintenant un homme debout à côté du jeu de cartes dont la mission est d'itérer la collection appelée items
. Mais ensuite, vous déplacez le signe du deck vers le man. Lorsque vous demandez à l'homme le premier "article" - il cherche la collection marquée "articles" - qui est maintenant lui! Il se demande donc le premier élément, où se situe la référence circulaire.
Lorsque vous affectez le résultat à une variable nouvelle, vous avez alors une requête qui itère sur une collection différente, et ne donne donc pas lieu à une boucle infinie.
Lorsque vous appelez ToList
, vous hydratez la requête dans une nouvelle collection et n'obtenez pas non plus de boucle infinie.
D'autres choses qui briseraient la référence circulaire:
ToList
items
à une autre variable et référencer que dans le lambda.Après avoir étudié les deux réponses données et fouillé un peu, j'ai trouvé un petit programme qui illustre 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 appelez cela avec:
var newList = GoNuts(new[]{1, 2, 3, 4, 5, 6});
Vous obtiendrez cette sortie à plusieurs reprises jusqu'à ce que vous obteniez enfin StackOverflowException
.
Select item = 1
GetFirst 1
Select item = 1
GetFirst 1
Select item = 1
GetFirst 1
...
Ce que cela montre est exactement ce que dasblinkenlight a précisé dans sa réponse mise à jour: la requête entre 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 cela, cela réussit. Pourquoi? Parce que dans ce cas, il est clair que l'appel à GetFirst
transmet une référence aux éléments d'origine qui ont été passés à la méthode. Dans le premier cas, GetFirst
transmet une référence à la collection newitems
, qui n'a pas encore été réalisée. À son tour, GetFirst
dit: "Hé, je dois énumérer cette collection." Et commence ainsi le premier appel récursif qui mène finalement à StackOverflowException
.
Fait intéressant, j'avais raison et mal quand j'ai dit qu'il consommait sa propre sortie. Le Select
consomme l'entrée d'origine, comme je m'y attendais. First
essaie de consommer la sortie.
Beaucoup de leçons à tirer ici. Pour moi, le plus important est "ne modifiez pas la valeur des paramètres d'entrée".
Merci à dasblinkenlight, D Stanley et Lucas Trzesniewski pour leur aide.