Pourquoi List <T> .ForEach permet-il de modifier sa liste?

90

Si j'utilise:

var strings = new List<string> { "sample" };
foreach (string s in strings)
{
  Console.WriteLine(s);
  strings.Add(s + "!");
}

le Adddans le foreachjette une InvalidOperationException (la collection a été modifiée; l'opération d'énumération peut ne pas s'exécuter), ce que je considère comme logique, car nous tirons le tapis de sous nos pieds.

Cependant, si j'utilise:

var strings = new List<string> { "sample" };
strings.ForEach(s =>
  {
    Console.WriteLine(s);
    strings.Add(s + "!");
  });

il se tire rapidement dans le pied en faisant une boucle jusqu'à ce qu'il lance une OutOfMemoryException.

Cela me surprend, car j'ai toujours pensé que List.ForEach n'était qu'un wrapper pour foreachou pour for.
Quelqu'un at-il une explication sur le comment et le pourquoi de ce comportement?

(Inspiré par la boucle ForEach pour une liste générique répétée à l'infini )

SWeko
la source
7
Je suis d'accord. C'est - douteux. Je voudrais que vous postez cela sur Microsoft Connect et demandez des éclaircissements.
TomTom
4
"Cela me surprend, car j'ai toujours pensé que List.ForEach n'était qu'un emballage pour foreachou pour for." Il pourrait encore utiliser for. Vous pouvez effectuer la même action dans une forboucle et générer la même OutOfMemoryException en conséquence.
Anthony Pegram
Ceci est basé sur ma question: stackoverflow.com/q/9311272/132239 , merci SWeko pour entrer dans ses détails
Kasrak

Réponses:

68

C'est parce que la ForEachméthode n'utilise pas l'énumérateur, elle parcourt les éléments avec une forboucle:

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        action(this._items[i]);
    }
}

(code obtenu avec JustDecompile)

Puisque l'énumérateur n'est pas utilisé, il ne vérifie jamais si la liste a changé et la condition de fin de la forboucle n'est jamais atteinte car elle _sizeest augmentée à chaque itération.

Thomas Levesque
la source
Ouais, mais comment est _sizecalculé? Si c'est juste pré-calculé, si devrait être exécuté une fois pour mon exemple. Il est évidemment rafraîchi d'une manière ou d'une autre.
SWeko
7
Il est actualisé à Add method -> this._items [this._size ++] = item;
Fabio
1
@SWeko, ce n'est pas calculé, il est mis à jour à chaque fois qu'un élément est ajouté ou supprimé.
Thomas Levesque
1
Il existe une _versionvariable privée List<T>qui pourrait détecter ce type de scénarios, car elle est mise à jour sur les opérations qui modifient la liste elle-même.
SWeko
Vous pouvez éviter les exceptions en obtenant d'abord la taille (int theSize = this._size), puis en l'utilisant dans la boucle for?
Lazlow
14

List<T>.ForEachest implémenté à l' forintérieur, il n'utilise donc pas d'énumérateur et permet de modifier la collection.

Alexey Raga
la source
6

Parce que le ForEach attaché à la classe List utilise en interne une boucle for qui est directement attachée à ses membres internes - que vous pouvez voir en téléchargeant le code source du framework .NET.

http://referencesource.microsoft.com/netframework.aspx

Où en tant que boucle foreach est avant tout une optimisation du compilateur mais doit également opérer contre la collection en tant qu'observateur - donc si la collection est modifiée, elle lève une exception.

Mike Perrenoud
la source
Et pour répondre au commentaire sur @Thomas post sur la façon dont il s'actualise - les membres internes sont actualisés lorsque add est appelé, c'est pourquoi il est capable de suivre les changements. Si vous deviez effectuer une insertion, à un index inférieur à celui actuel, vous n'opéreriez jamais sur cet élément car il est déjà itéré au-delà de cet élément. Mais puisque vous ajoutez à la fin, cela fonctionne.
Mike Perrenoud
1
Oui, changer la Addligne avec strings.Insert(0, s + "!")juste «échantillon» imprime. Il est étrange que cela ne soit pas du tout mentionné dans la documentation.
SWeko
Eh bien, je pense que Microsoft s'est rendu compte qu'il est pratiquement impossible de fournir toutes les mises en garde qui existent dans leur documentation - ils fournissent donc leur code source maintenant. Honnêtement, je trouve que c'est une meilleure solution, mais le seul problème que j'ai trouvé est que les produits comme WF ne sont pas mis à jour aussi rapidement - le code source 4.x WF n'est toujours pas disponible.
Mike Perrenoud
4

Nous connaissons ce problème, c'était un oubli lors de sa rédaction initiale. Malheureusement, nous ne pouvons pas le changer car cela empêcherait maintenant ce code précédemment fonctionnel de s'exécuter:

        var list = new List<string>();
        list.Add("Foo");
        list.Add("Bar");

        list.ForEach((item) => 
        { 
            if(item=="Foo") 
                list.Remove(item); 
        });

L'utilité de cette méthode elle-même est discutable comme l'a souligné Eric Lippert , nous ne l'avons donc pas incluse pour les applications de style .NET pour Metro (c'est-à-dire les applications Windows 8).

David Kean (équipe BCL)

David Kean
la source
1
Je vois que ce serait un grand changement de rupture, mais néanmoins il peut échouer de manière non évidente, et ce n'est jamais une bonne chose. Je ne vois pas de scénario où l'utilisation de la méthode ForEach est supérieure à un simple for (ou foreach si la modification de la liste d'origine n'est pas requise)
SWeko