0
votes

Pourquoi certains Enumerable peuvent-ils être modifiés à l'intérieur de foreach et d'autres non?

J'ai trouvé un comportement intéressant du résultat des requêtes LINQ en travaillant avec C #. J'essaie de comprendre cela, mais je n'ai pas pu trouver une explication appropriée de la raison pour laquelle cela fonctionne tel quel. Donc je demande ici, peut-être que quelqu'un peut me donner une bonne explication (du fonctionnement interne qui conduit à ce comportement) ou peut-être quelques liens.

J'ai cette classe:

foreach (var value in valuesToInsert.ToList())


4 commentaires

La question elle-même mise à part, le code semble conçu pour ce qu'il entend réaliser. Si vous voulez le parent et ses enfants, vous pouvez faire list.Where (x => x.Id == 1 || x.ParentId == 1) . Si vous ne voulez que les enfants, list.Where (x => x.ParentId == 1)


C'est du code simplifié, en réalité l'imbrication pourrait être pas seulement à deux niveaux.


Savez-vous que dans une instruction comme var testEn = list.Where (x => x.Id == 1); , testEn est juste une "vue" de < code> list avec uniquement les éléments qui correspondent au prédicat, et qu'aucune nouvelle liste n'est créée jusqu'à ce que vous appeliez ToList () ? Faire une boucle sur testEn est comme faire une boucle sur liste , mais avec une condition.


"Pas de nouvelles listes" - vous ne voulez pas dire aucune nouvelle collections en mémoire?


3 Réponses :


0
votes

Il y a plusieurs questions ici:

Ainsi, après la première requête, le nombre de variables de résultat est 1, après la deuxième requête, le nombre de valuesToInsert est de 2, et après la boucle foreach (qui ne modifie pas explicitement valuesToInsert), le nombre de valuesToInsert change.

C'est comme prévu car la référence que nous avons dans la variable est la même que celle de la variable valuesToInsert . L'objet est donc le même mais plusieurs références pointent vers le même.

Votre deuxième question:

Alors, pourquoi la valeur de cet Enumerable peut-elle être modifiée dans foreach?

La collection IEnumerable est en lecture seule lorsque nous avons la collection comme référence de type IEnumerable mais lorsque nous appelons la méthode ToList () dessus, nous avons une copie de la collection qui pointe vers la même collection originale mais nous pouvons maintenant ajouter plus d'éléments à la collection.

Lorsque nous avons une collection comme IEnumerable , la collection peut être itérée et lue, mais l'ajout de plus d'éléments lors de l'énumération échouerait car la collection est censée être lue séquentiellement.

Thrid:

Il ne fait que deux itérations.

Oui, car à ce moment-là, quel que soit le nombre d'éléments dans la collection, ils ont été énumérés et les références à celle-ci ont été stockées sous forme de nouvelle liste alors qu'elle pointe toujours vers le même objet, c'est-à-dire IEnumerable, mais maintenant nous pouvons ajouter d'autres éléments en raison de son type comme Liste.

Voir:

var result = list.Where(x => x.Id == 1).ToList(); 
// result is collection which can be modified, items add, remove etc

var result = list.Where(x => x.Id == 1);
 // result is IEnumerable which can be iterated to get items one by one
 // modifying this collection would error out normally


2 commentaires

> Lorsque nous avons une collection comme IEnumerable, la collection peut être itérée et lue, mais l'ajout de plus d'éléments lors de l'énumération échouerait car la collection est censée être lue séquentiellement. Que voulez-vous dire par "l'ajout échouerait lors de l'énumération"? Nous ne pouvons évidemment pas utiliser la méthode .Add () sur IEnumerable, mais dans mon code, les objets s'ajoutent à la collection IEnumerable lors de l'énumération (au début de la collection foreach valuesToInsert n'a que deux objets, puis il passe à trois, et foreach, au total, faisant trois itérations).


Mais vous ajoutez en utilisant une référence de liste



0
votes

La collection valuesToInsert a une référence à la collection result dans la clause Where :

foreach (var value in valuesToInsert.ToList())

Comme un Enumerable fonctionne avec le rendement de rendement, il utilise la collection result la plus récente pour chaque élément produit.

Si vous ne souhaitez pas ce comportement, vous devez d'abord évaluer valueToInsert en utilisant ToList()

var valuesToInsert = list.Where(x => result.Any(y => y.Id == x.ParentId));

Concernant l'exception "La collection a été modifiée". Vous ne pouvez pas modifier un énumérable pendant son énumération. Maintenant, la collection result est modifiée mais pas pendant son énumération; il n'est énuméré qu'à chaque fois que le pour chaque boucle demande un nouvel élément. (Cela rend votre algorithme pour ajouter les enfants moins efficace, ce qui deviendra perceptible pour les grandes collections.)


1 commentaires

Mon algorithme doit ajouter non seulement des enfants, mais des enfants d'enfants, etc. Vous savez l'écrire plus efficace?



0
votes

Ce bloc de code:

IEnumerator<A> enumerator = valuesToInsert.GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
        var value = enumerator.Current;
        result.Add(value);
    }
}
finally
{
    enumerator.Dispose();
}

... est transformé par le compilateur C # en ce bloc de code équivalent:

foreach (var value in valuesToInsert)
{
    result.Add(value);
}

L'énumérateur renvoyé par une List est invalidé lorsqu'un List est muté, ce qui signifie que la méthode MoveNext lancera une InvalidOperationException si elle est invoquée après une mutation. Dans ce cas, valuesToInsert n'est pas une List , mais un énumérable renvoyé par la méthode LINQ Where . Cette méthode fonctionne en énumérant l'énumérateur qu'il obtient paresseusement par sa source, qui dans ce cas est la liste . Ainsi, l'énumération d'un énumérateur provoque indirectement l'énumération d'un autre, qui est caché plus profondément dans la chaîne LINQ magique. Dans le premier cas, la liste n'est pas mutée à l'intérieur du bloc d'énumération, donc aucune exception n'est levée. Dans le second cas, il est muté, provoquant une exception qui est propagée de l'un MoveNext à l'autre, et finalement levée par l'instruction foreach .

Il est à noter que ce comportement ne fait pas partie du contrat public de la classe List , il pourrait donc être modifié dans une future version de .NET. Vous devriez donc probablement éviter de dépendre de ce comportement pour l'exactitude de votre programme. Cet avertissement n'est pas théorique. Un changement comme celui-ci s'est déjà produit avec la classe Dictionary dans .NET Core 3.0.


2 commentaires

Quelle partie du comportement pourrait être modifiée? Que la méthode MoveNext lèvera une InvalidOperationException si elle est invoquée après une mutation?


@helgez oui, ça. Dans une version future de .NET, l'exception peut ne pas être levée.