foreach vs someList.ForEach () {}


167

Aparentemente, hay muchas formas de iterar sobre una colección. Curioso si hay alguna diferencia, o por qué usarías una forma sobre la otra.

Primer tipo:

List<string> someList = <some way to init>
foreach(string s in someList) {
   <process the string>
}

Otra manera:

List<string> someList = <some way to init>
someList.ForEach(delegate(string s) {
    <process the string>
});

Supongo que, en lugar del delegado anónimo que uso arriba, tendrías un delegado reutilizable que podrías especificar ...


Respuestas:


134

Hay una distinción importante y útil entre los dos.

Debido a que .ForEach usa un forbucle para iterar la colección, esto es válido (edición: antes de .net 4.5 - la implementación cambió y ambos arrojan):

someList.ForEach(x => { if(x.RemoveMe) someList.Remove(x); }); 

mientras que foreachusa un enumerador, por lo que esto no es válido:

foreach(var item in someList)
  if(item.RemoveMe) someList.Remove(item);

tl; dr: ¡NO copiar este código en su aplicación!

Estos ejemplos no son las mejores prácticas, son solo para demostrar las diferencias entre ForEach()y foreach.

Eliminar elementos de una lista dentro de un forciclo puede tener efectos secundarios. El más común se describe en los comentarios a esta pregunta.

En general, si desea eliminar varios elementos de una lista, desearía separar la determinación de qué elementos eliminar de la eliminación real. No mantiene su código compacto, pero garantiza que no se perderá ningún artículo.


35
incluso entonces, deberías usar someList.RemoveAll (x => x.RemoveMe) en su lugar
Mark Cidade

2
Con Linq, todo se puede hacer mejor. Solo mostraba un ejemplo de modificación de la colección dentro de foreach ...

2
RemoveAll () es un método en la Lista <T>.
Mark Cidade el

3
Probablemente esté al tanto de esto, pero las personas deben tener cuidado al eliminar elementos de esta manera; si elimina el elemento N, la iteración omitirá el elemento (N + 1) y no lo verá en su delegado ni tendrá la oportunidad de eliminarlo, como si hiciera esto en su propio ciclo for.
El Zorko

9
Si itera la lista hacia atrás con un bucle for, puede eliminar elementos sin ningún problema de cambio de índice.
S. Tarık Çetin

78

Teníamos un código aquí (en VS2005 y C # 2.0) donde los ingenieros anteriores se esforzaron por usar en list.ForEach( delegate(item) { foo;});lugar de foreach(item in list) {foo; };todo el código que escribieron. por ejemplo, un bloque de código para leer filas de un dataReader.

Todavía no sé exactamente por qué hicieron esto.

Los inconvenientes de list.ForEach()son:

  • Es más detallado en C # 2.0. Sin embargo, en C # 3 en adelante, puede usar la =>sintaxis " " para hacer algunas expresiones bien concisas.

  • Es menos familiar. Las personas que tienen que mantener este código se preguntarán por qué lo hicieron de esa manera. Me tomó un tiempo decidir que no había ninguna razón, excepto quizás hacer que el escritor pareciera inteligente (la calidad del resto del código lo socavaba). También era menos legible, con el " })" al final del bloque de código de delegado.

  • Vea también el libro de Bill Wagner "C # efectivo: 50 formas específicas de mejorar su C #" donde habla sobre por qué se prefiere foreach a otros bucles como for o while, el punto principal es que está dejando que el compilador decida la mejor manera de construir el lazo. Si una versión futura del compilador logra utilizar una técnica más rápida, obtendrá esto de forma gratuita mediante el uso de foreach y la reconstrucción, en lugar de cambiar su código.

  • una foreach(item in list)construcción le permite usar breako continuesi necesita salir de la iteración o del bucle. Pero no puede alterar la lista dentro de un bucle foreach.

Me sorprende ver que list.ForEaches un poco más rápido. Pero probablemente esa no sea una razón válida para usarlo, sería una optimización prematura. Si su aplicación utiliza una base de datos o un servicio web que, no el control de bucle, casi siempre estará donde sea que pase el tiempo. ¿Y también lo has comparado con un forbucle? El list.ForEachpodría ser más rápido debido al uso interno yfor bucle sin el envoltorio sería aún más rápido.

No estoy de acuerdo con que el list.ForEach(delegate) versión sea "más funcional" de manera significativa. Transfiere una función a una función, pero no hay una gran diferencia en el resultado o la organización del programa.

No creo que foreach(item in list)"diga exactamente cómo quieres que se haga": un for(int 1 = 0; i < count; i++)bucle hace eso, unforeach bucle deja la elección del control al compilador.

Mi intención es, en un nuevo proyecto, utilizar la foreach(item in list)mayoría de los bucles para cumplir con el uso común y facilitar la lectura, y usar list.Foreach()solo para bloques cortos, cuando puede hacer algo de manera más elegante o compacta con el =>operador C # 3 " ". En casos como ese, puede haber un método de extensión LINQ que sea más específico que ForEach(). A ver si Where(), Select(), Any(), All(), Max()o uno de los muchos otros métodos de LINQ no ya hacer lo que desee en el bucle.


Solo por curiosidad ... mira la implementación de Microsoft ... referencesource.microsoft.com/#mscorlib/system/collections/…
Alexandre

17

Por diversión, metí List en reflector y este es el C # resultante:

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

Del mismo modo, MoveNext en Enumerator, que es lo que usa foreach es este:

public bool MoveNext()
{
    if (this.version != this.list._version)
    {
        ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
    }
    if (this.index < this.list._size)
    {
        this.current = this.list._items[this.index];
        this.index++;
        return true;
    }
    this.index = this.list._size + 1;
    this.current = default(T);
    return false;
}

List.ForEach está mucho más recortado que MoveNext, mucho menos procesamiento, lo más probable es que JIT se convierta en algo eficiente.

Además, foreach () asignará un nuevo enumerador sin importar qué. El GC es tu amigo, pero si estás haciendo el mismo foreach repetidamente, esto generará más objetos descartables, en lugar de reutilizar el mismo delegado, PERO , este es realmente un caso marginal. En el uso típico, verá poca o ninguna diferencia.


1
No tiene garantía de que el código generado por foreach sea el mismo entre las versiones del compilador. El código generado puede mejorarse en una versión futura.
Anthony

1
A partir de .NET Core 3.1 ForEach es aún más rápido.
Jackmott

13

Sé dos cosas oscuras que las hacen diferentes. Ve a mi

En primer lugar, está el error clásico de hacer un delegado para cada elemento de la lista. Si usa la palabra clave foreach, todos sus delegados pueden terminar refiriéndose al último elemento de la lista:

    // A list of actions to execute later
    List<Action> actions = new List<Action>();

    // Numbers 0 to 9
    List<int> numbers = Enumerable.Range(0, 10).ToList();

    // Store an action that prints each number (WRONG!)
    foreach (int number in numbers)
        actions.Add(() => Console.WriteLine(number));

    // Run the actions, we actually print 10 copies of "9"
    foreach (Action action in actions)
        action();

    // So try again
    actions.Clear();

    // Store an action that prints each number (RIGHT!)
    numbers.ForEach(number =>
        actions.Add(() => Console.WriteLine(number)));

    // Run the actions
    foreach (Action action in actions)
        action();

El método List.ForEach no tiene este problema. El elemento actual de la iteración se pasa por valor como argumento a la lambda externa, y luego la lambda interna captura correctamente ese argumento en su propio cierre. Problema resuelto.

(Lamentablemente, creo que ForEach es miembro de List, en lugar de un método de extensión, aunque es fácil definirlo usted mismo para que tenga esta facilidad en cualquier tipo enumerable).

En segundo lugar, el enfoque del método ForEach tiene una limitación. Si está implementando IEnumerable mediante el rendimiento de rendimiento, no puede hacer un rendimiento dentro de la lambda. Por lo tanto, este método no permite recorrer los elementos de una colección para obtener resultados. Tendrá que usar la palabra clave foreach y evitar el problema de cierre haciendo manualmente una copia del valor del bucle actual dentro del bucle.

Más aquí


55
El problema con el que menciona foreachestá "solucionado" en C # 5. stackoverflow.com/questions/8898925/…
StriplingWarrior

13

Supongo que la someList.ForEach()llamada podría paralelizarse fácilmente, mientras que lo normal foreachno es tan fácil de ejecutar en paralelo. Podría ejecutar fácilmente varios delegados diferentes en diferentes núcleos, lo que no es tan fácil de hacer con un normal foreach.
Solo mis 2 centavos


2
Creo que quería decir que el motor de tiempo de ejecución podría paralelizarlo automáticamente. De lo contrario, tanto Foreach como .ForEach se pueden paralelizar a mano usando un hilo del grupo en cada delegado de acción
Isak Savo

@Isak pero eso sería una suposición incorrecta. Si el método anónimo levanta a un local o miembro, el tiempo de ejecución no será fácil) paralelizar
Rune FS

6

Como dicen, el diablo está en los detalles ...

La mayor diferencia entre los dos métodos de enumeración de colecciones es que foreachlleva el estado, mientras ForEach(x => { })que no.

Pero profundicemos un poco más, porque hay algunas cosas que debe tener en cuenta que pueden influir en su decisión, y hay algunas advertencias que debe tener en cuenta al codificar para cualquier caso.

Usemos List<T>en nuestro pequeño experimento para observar el comportamiento. Para este experimento, estoy usando .NET 4.7.2:

var names = new List<string>
{
    "Henry",
    "Shirley",
    "Ann",
    "Peter",
    "Nancy"
};

Vamos a iterar sobre esto con foreachprimero:

foreach (var name in names)
{
    Console.WriteLine(name);
}

Podríamos ampliar esto en:

using (var enumerator = names.GetEnumerator())
{

}

Con el enumerador en la mano, mirando debajo de las cubiertas obtenemos:

public List<T>.Enumerator GetEnumerator()
{
  return new List<T>.Enumerator(this);
}
    internal Enumerator(List<T> list)
{
  this.list = list;
  this.index = 0;
  this.version = list._version;
  this.current = default (T);
}

public bool MoveNext()
{
  List<T> list = this.list;
  if (this.version != list._version || (uint) this.index >= (uint) list._size)
    return this.MoveNextRare();
  this.current = list._items[this.index];
  ++this.index;
  return true;
}

object IEnumerator.Current
{
  {
    if (this.index == 0 || this.index == this.list._size + 1)
      ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
    return (object) this.Current;
  }
}

Dos cosas se hacen evidentes de inmediato:

  1. Se nos devuelve un objeto con estado con conocimiento íntimo de la colección subyacente.
  2. La copia de la colección es una copia superficial.

Por supuesto, esto no es seguro para subprocesos. Como se señaló anteriormente, cambiar la colección mientras se itera es un mal mojo.

Pero, ¿qué pasa con el problema de que la colección se vuelva inválida durante la iteración por medios ajenos a la manipulación de la colección durante la iteración? Las mejores prácticas sugieren versionar la colección durante las operaciones y la iteración, y verificar versiones para detectar cuándo cambia la colección subyacente.

Aquí es donde las cosas se ponen realmente turbias. De acuerdo con la documentación de Microsoft:

Si se realizan cambios en la colección, como agregar, modificar o eliminar elementos, el comportamiento del enumerador no está definido.

Bueno, qué significa eso? A modo de ejemplo, solo porque List<T>implementa el manejo de excepciones no significa que todas las colecciones que implementan IList<T>harán lo mismo. Eso parece ser una clara violación del Principio de sustitución de Liskov:

Los objetos de una superclase serán reemplazables por objetos de sus subclases sin interrumpir la aplicación.

Otro problema es que el enumerador debe implementar IDisposable, lo que significa otra fuente de posibles pérdidas de memoria, no solo si la persona que llama se equivoca, sino si el autor no implementaDispose patrón correctamente.

Por último, tenemos un problema de por vida ... ¿qué sucede si el iterador es válido, pero la colección subyacente se ha ido? Ahora tenemos una instantánea de lo que era ... cuando separas la vida útil de una colección y sus iteradores, estás pidiendo problemas.

Ahora examinemos ForEach(x => { }):

names.ForEach(name =>
{

});

Esto se expande a:

public void ForEach(Action<T> action)
{
  if (action == null)
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
  int version = this._version;
  for (int index = 0; index < this._size && (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5); ++index)
    action(this._items[index]);
  if (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
    return;
  ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}

De nota importante es la siguiente:

for (int index = 0; index < this._size && ... ; ++index) action(this._items[index]);

Este código no asigna ningún enumerador (nada a Dispose), y no se detiene mientras itera.

Tenga en cuenta que esto también realiza una copia superficial de la colección subyacente, pero la colección ahora es una instantánea en el tiempo. Si el autor no implementa correctamente una comprobación para que la colección cambie o quede "obsoleta", la instantánea sigue siendo válida.

Esto de ninguna manera lo protege del problema de los problemas de por vida ... si la colección subyacente desaparece, ahora tiene una copia superficial que señala lo que fue ... pero al menos no tiene un Dispose problema para tratar con iteradores huérfanos ...

Sí, dije iteradores ... a veces es ventajoso tener estado. Suponga que desea mantener algo similar a un cursor de base de datos ... tal vez el foreachestilo Iterator<T>a seguir sea ​​el de varios estilos . Personalmente, no me gusta este estilo de diseño, ya que hay demasiados problemas de por vida, y confías en las buenas gracias de los autores de las colecciones en las que confías (a menos que literalmente escribas todo tú mismo desde cero).

Siempre hay una tercera opción ...

for (var i = 0; i < names.Count; i++)
{
    Console.WriteLine(names[i]);
}

No es sexy, pero tiene dientes (disculpas a Tom Cruise y la película The Firm )

Es su elección, pero ahora lo sabe y puede ser informado.


5

Podrías nombrar al delegado anónimo :-)

Y puedes escribir el segundo como:

someList.ForEach(s => s.ToUpper())

Lo cual prefiero, y ahorra mucho tipeo.

Como dice Joachim, el paralelismo es más fácil de aplicar a la segunda forma.


2

Detrás de escena, el delegado anónimo se convierte en un método real para que pueda tener algunos gastos generales con la segunda opción si el compilador no eligió incorporar la función. Además, cualquier variable local referenciada por el cuerpo del ejemplo de delegado anónimo cambiaría de naturaleza debido a los trucos del compilador para ocultar el hecho de que se compila a un nuevo método. Más información aquí sobre cómo C # hace esta magia:

http://blogs.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx


2

La función ForEach es miembro de la clase genérica Lista.

He creado la siguiente extensión para reproducir el código interno:

public static class MyExtension<T>
    {
        public static void MyForEach(this IEnumerable<T> collection, Action<T> action)
        {
            foreach (T item in collection)
                action.Invoke(item);
        }
    }

Entonces, al final, estamos usando un foreach normal (o un bucle si lo desea).

Por otro lado, usar una función delegada es solo otra forma de definir una función, este código:

delegate(string s) {
    <process the string>
}

es equivalente a:

private static void myFunction(string s, <other variables...>)
{
   <process the string>
}

o usando expresiones labda:

(s) => <process the string>

2

Todo el alcance de ForEach (función delegada) se trata como una sola línea de código (que llama a la función), y no puede establecer puntos de interrupción o ingresar al código. Si se produce una excepción no controlada, se marca todo el bloque.


2

List.ForEach () se considera más funcional.

List.ForEach()dice lo que quieres que se haga. foreach(item in list)también dice exactamente cómo quieres que se haga. Esto deja List.ForEachlibre para cambiar la implementación de la parte de cómo en el futuro. Por ejemplo, una futura versión hipotética de .Net siempre podría ejecutarseList.ForEach en paralelo, bajo el supuesto de que en este punto todos tienen varios núcleos de CPU que generalmente están inactivos.

Por otro lado, foreach (item in list)te da un poco más de control sobre el ciclo. Por ejemplo, sabe que los elementos se iterarán en algún tipo de orden secuencial, y podría dividirse fácilmente en el medio si un elemento cumple alguna condición.


Algunos comentarios más recientes sobre este tema están disponibles aquí:

https://stackoverflow.com/a/529197/3043


1

La segunda forma que mostró utiliza un método de extensión para ejecutar el método delegado para cada uno de los elementos de la lista.

De esta manera, tiene otra llamada de delegado (= método).

Además, existe la posibilidad de iterar la lista con un bucle for .


1

Una cosa a tener en cuenta es cómo salir del método genérico .ForEach - vea esta discusión . Aunque el enlace parece decir que de esta manera es el más rápido. No estoy seguro de por qué: pensarías que serían equivalentes una vez compilados ...


0

Hay una manera, lo he hecho en mi aplicación:

List<string> someList = <some way to init>
someList.ForEach(s => <your actions>);

Puedes usarlo como tu artículo desde el foreach

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.