¿Por qué List <T> .ForEach permite modificar su lista?

90

Si uso:

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

el Adden el foreacharroja una InvalidOperationException (la colección fue modificada; la operación de enumeración puede no ejecutarse), lo cual considero lógico, ya que estamos tirando de la alfombra debajo de nuestros pies.

Sin embargo, si uso:

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

rápidamente se dispara en el pie haciendo un bucle hasta que arroja una OutOfMemoryException.

Esto me sorprende, ya que siempre pensé que List.ForEach era solo un envoltorio para foreacho para for.
¿Alguien tiene una explicación del cómo y el por qué de este comportamiento?

(Inspirado por el bucle ForEach para una lista genérica repetida sin cesar )

SWeko
fuente
7
Estoy de acuerdo. Esto es ... dudoso. Le sugiero que publique eso en microsoft connect y pida una aclaración.
TomTom
4
"Esto me sorprende, ya que siempre pensé que List.ForEach era simplemente un envoltorio para foreacho para for". Todavía podría usar for. Puede realizar la misma acción en un forbucle y generar la misma OutOfMemoryException como resultado.
Anthony Pegram
Esto se basa en mi pregunta: stackoverflow.com/q/9311272/132239 , gracias a SWeko por entrar en detalles
Kasrak

Respuestas:

68

Es porque el ForEachmétodo no usa el enumerador, recorre los elementos con un forbucle:

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

(código obtenido con JustDecompile)

Dado que el enumerador no se utiliza, nunca comprueba si la lista ha cambiado y la condición final del forciclo nunca se alcanza porque _sizeaumenta en cada iteración.

Thomas Levesque
fuente
Sí, pero ¿cómo se _sizecalcula? Si solo está precalculado, debería ejecutarse una vez para mi ejemplo. Obviamente, está actualizado de alguna manera.
SWeko
7
Se actualiza en Agregar método -> this._items [this._size ++] = item;
Fabio
1
@SWeko, no se calcula, se actualiza cada vez que se agrega o elimina un elemento.
Thomas Levesque
1
Existe una _versionvariable privada en la List<T>que podría detectar este tipo de escenarios, ya que se actualiza sobre las operaciones que cambian la propia lista.
SWeko
Podría evitar excepciones obteniendo primero el tamaño (int theSize = this._size) y luego usándolo dentro del bucle for?
Lazlow
14

List<T>.ForEachse implementa por fordentro, por lo que no utiliza enumerador y permite modificar la colección.

Alexey Raga
fuente
6

Porque el ForEach adjunto a la clase List utiliza internamente un bucle for que está directamente adjunto a sus miembros internos, que puede ver descargando el código fuente para el marco .NET.

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

Mientras que un bucle foreach es, ante todo, una optimización del compilador, pero también debe operar contra la colección como observador, por lo que si la colección se modifica, lanza una excepción.

Mike Perrenoud
fuente
Y para responder al comentario en la publicación de @Thomas sobre cómo se actualiza, los miembros internos se actualizan cuando se llama a add, por eso puede mantenerse al día con los cambios. Si tuviera que realizar una inserción, en un índice menor que el actual, nunca operaría en ese elemento porque ya se ha iterado más allá de ese elemento. Pero como estás agregando al final, funciona.
Mike Perrenoud
1
Sí, cambiando la Addlínea con strings.Insert(0, s + "!")solo imprime 'muestra'. Es extraño que esto no se mencione en absoluto en la documentación.
SWeko
Bueno, creo que Microsoft se dio cuenta de que es casi imposible proporcionar todas las advertencias que existen en su documentación, por lo que ahora proporcionan su código fuente. Honestamente, encuentro que es una mejor solución, pero el único problema que he encontrado es que productos como WF no se actualizan tan rápido: el código fuente 4.x WF todavía no está disponible.
Mike Perrenoud
4

Sabemos de este problema, fue un descuido cuando se escribió originalmente. Desafortunadamente, no podemos cambiarlo porque ahora evitaría que se ejecute este código que anteriormente funcionaba:

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

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

La utilidad de este método en sí es cuestionable, como señaló Eric Lippert , por lo que no lo incluimos para .NET para aplicaciones de estilo Metro (es decir, aplicaciones de Windows 8).

David Kean (equipo BCL)

David Kean
fuente
1
Veo que este sería un gran cambio de ruptura, pero no obstante, puede fallar de maneras no obvias, y eso nunca es bueno. No puedo ver un escenario en el que el uso del método ForEach sea superior a un simple for (o foreach si no se requiere
modificar