¿Por qué no hay un método de extensión ForEach en IEnumerable?

389

Inspirado por otra pregunta sobre la Zipfunción que falta :

¿Por qué no hay un ForEachmétodo de extensión en la Enumerableclase? O en cualquier lugar? La única clase que obtiene un ForEachmétodo es List<>. ¿Hay alguna razón por la que falta (rendimiento)?

Cameron MacFarland
fuente
Probablemente es como se insinuó en una publicación anterior, ya que foreach es compatible como una palabra clave de lenguaje en C # y VB.NET (y muchas otras) La mayoría de las personas (incluido yo mismo) simplemente escriben una ellos mismos según sea necesario y apropiado.
chrisb
2
Pregunta relacionada aquí con enlace a 'respuesta oficial' stackoverflow.com/questions/858978/…
Benjol
Creo que un punto muy importante es que un bucle foreach es más fácil de detectar. Si usa .ForEach (...) es fácil perderse este, cuando mira el código. Esto se vuelve importante cuando tienes problemas de rendimiento. Por supuesto, .ToList () y .ToArray () tienen el mismo problema, pero se usan un poco diferentes. Otra razón podría ser que es más común en un .ForEach modificar la lista fuente (por ejemplo, eliminar / agregar elementos) para que ya no sea una consulta "pura".
Daniel Bişar
Al depurar código paralelizada, a menudo trato de intercambio Parallel.ForEachpara Enumerable.ForEachsólo para descubrir esta última no existe. C # se perdió un truco para facilitar las cosas aquí.
Coronel Panic

Respuestas:

198

Ya hay una declaración foreach incluida en el lenguaje que hace el trabajo la mayor parte del tiempo.

Odiaría ver lo siguiente:

list.ForEach( item =>
{
    item.DoSomething();
} );

En vez de:

foreach(Item item in list)
{
     item.DoSomething();
}

Este último es más claro y más fácil de leer en la mayoría de las situaciones , aunque quizás sea un poco más largo para escribir.

Sin embargo, debo admitir que cambié mi postura sobre ese tema; un método de extensión ForEach () sería realmente útil en algunas situaciones.

Estas son las principales diferencias entre la declaración y el método:

  • Verificación de tipos: foreach se realiza en tiempo de ejecución, ForEach () está en tiempo de compilación (¡Big Plus!)
  • La sintaxis para llamar a un delegado es, de hecho, mucho más simple: objects.ForEach (DoSomething);
  • ForEach () podría estar encadenado: aunque la maldad / utilidad de dicha característica está abierta a discusión.

Esos son todos grandes puntos hechos por muchas personas aquí y puedo ver por qué las personas no tienen la función. No me importaría que Microsoft agregue un método ForEach estándar en la próxima iteración del marco.

Coincoin
fuente
39
La discusión aquí da la respuesta: forums.microsoft.com/MSDN/... Básicamente, se tomó la decisión de mantener los métodos de extensión funcionalmente "puros". Un ForEach fomentaría los efectos secundarios al usar los métodos de extensión, que no era la intención.
mancaus
17
'foreach' puede parecer más claro si tiene un fondo C, pero la iteración interna establece más claramente su intención. Eso significa que podría hacer cosas como 'secuencia.AsParallel (). ForEach (...)'.
Jay Bazuzi
10
No estoy seguro de que su punto sobre la verificación de tipo sea correcto. foreachse verifica el tipo en tiempo de compilación si el enumerable está parametrizado. Solo le falta la comprobación del tipo de tiempo de compilación para colecciones no genéricas, como lo haría un ForEachmétodo de extensión hipotético .
Richard Poole
49
Método de extensión sería muy útil en LINQ cadena con notación de puntos, como: col.Where(...).OrderBy(...).ForEach(...). Sintaxis mucho más simple, sin contaminación de pantalla con variables locales redundantes o corchetes largos en foreach ().
Jacek Gorgoń
22
"Odiaría ver lo siguiente": la pregunta no se refería a sus preferencias personales, e hizo que el código fuera más odioso al agregar saltos de línea extraños y un par de llaves extrañas. No es tan odioso es list.ForEach(item => item.DoSomething()). ¿Y quién dice que es una lista? El caso de uso obvio es al final de una cadena; con la declaración foreach, tendría que colocar toda la cadena después in, y la operación a realizar dentro del bloque, que es mucho menos natural o consistente.
Jim Balter
73

El método ForEach se agregó antes de LINQ. Si agrega la extensión ForEach, nunca se llamará para instancias de Lista debido a restricciones de métodos de extensión. Creo que la razón por la que no se agregó es para no interferir con el existente.

Sin embargo, si realmente echa de menos esta pequeña y agradable función, puede implementar su propia versión

public static void ForEach<T>(
    this IEnumerable<T> source,
    Action<T> action)
{
    foreach (T element in source) 
        action(element);
}
aku
fuente
11
Los métodos de extensión permiten esto. Si ya hay una implementación de instancia de un nombre de método dado, se utiliza ese método en lugar del método de extensión. Por lo tanto, puede implementar un método de extensión ForEach y no hacer que choque con el método List <>. ForEach.
Cameron MacFarland
3
Cameron, créeme, sé cómo funcionan los métodos de extensión :) La lista hereda IEnumerable, por lo que no será algo obvio.
aku
99
De la Guía de programación de métodos de extensión ( msdn.microsoft.com/en-us/library/bb383977.aspx ): Nunca se llamará a un método de extensión con el mismo nombre y firma que una interfaz o método de clase. Me parece bastante obvio.
Cameron MacFarland
3
¿Estoy hablando de algo diferente? Creo que no es bueno cuando muchas clases tienen el método ForEach, pero algunas de ellas pueden funcionar de manera diferente.
aku
55
Una cosa interesante a tener en cuenta sobre List <>. ForEach y Array.ForEach es que en realidad no están utilizando la construcción foreach internamente. Ambos usan un plano para bucle. Tal vez sea una cuestión de rendimiento ( diditwith.net/2006/10/05/PerformanceOfForeachVsListForEach.aspx ). Pero dado que tanto Array como List implementan métodos ForEach, es sorprendente que al menos no implementaran un método de extensión para IList <>, si no también para IEnumerable <>.
Matt Dotson
53

Podrías escribir este método de extensión:

// Possibly call this "Do"
IEnumerable<T> Apply<T> (this IEnumerable<T> source, Action<T> action)
{
    foreach (var e in source)
    {
        action(e);
        yield return e;
    }
}

Pros

Permite encadenar:

MySequence
    .Apply(...)
    .Apply(...)
    .Apply(...);

Contras

En realidad, no hará nada hasta que haga algo para forzar la iteración. Por esa razón, no debería llamarse .ForEach(). Podrías escribir.ToList() al final, o también podría escribir este método de extensión:

// possibly call this "Realize"
IEnumerable<T> Done<T> (this IEnumerable<T> source)
{
    foreach (var e in source)
    {
        // do nothing
        ;
    }

    return source;
}

Esto puede ser una desviación demasiado significativa de las bibliotecas C # de envío; Los lectores que no estén familiarizados con sus métodos de extensión no sabrán qué hacer con su código.

Jay Bazuzi
fuente
1
Su primera función podría usarse sin el método 'realizar' si se escribe como:{foreach (var e in source) {action(e);} return source;}
jjnguy
3
¿No podría simplemente usar Seleccionar en lugar de su método Aplicar? Me parece que aplicar una acción a cada elemento y producirla es exactamente lo que hace Select. Por ejemplonumbers.Select(n => n*2);
rasmusvhansen
1
Creo que Apply es más legible que usar Select para este propósito. Al leer una instrucción Select, la leo como "obtener algo desde adentro" donde, como Aplicar, es "hacer algo de trabajo".
Dr. Rob Lang
2
@rasmusvhansen En realidad, Select()dice aplicar una función a cada elemento y devolver el valor de la función donde Apply()dice aplicar una Actiona cada elemento y devolver el elemento original .
NetMage
1
Esto es equivalente aSelect(e => { action(e); return e; })
Jim Balter
35

La discusión aquí da la respuesta:

En realidad, la discusión específica que presencié realmente dependía de la pureza funcional. En una expresión, con frecuencia se hacen suposiciones acerca de no tener efectos secundarios. Tener ForEach es específicamente invitar a los efectos secundarios en lugar de simplemente tolerarlos. - Keith Farmer (socio)

Básicamente, se tomó la decisión de mantener los métodos de extensión funcionalmente "puros". Un ForEach fomentaría los efectos secundarios al usar los métodos de extensión Enumerable, que no era la intención.

mancaus
fuente
66
Ver también blogs.msdn.com/b/ericlippert/archive/2009/05/18/… . Ddoing viola los principios de programación funcional en los que se basan todos los demás operadores de secuencia. Claramente, el único propósito de una llamada a este método es causar efectos secundarios
Michael Freidgeim
77
Ese razonamiento es completamente falso. El caso de uso es que se necesitan efectos secundarios ... sin ForEach, debe escribir un bucle foreach. La existencia de ForEach no fomenta los efectos secundarios y su ausencia no los reduce.
Jim Balter
Dado lo simple que es escribir el método de extensión, literalmente no hay razón para que no sea un lenguaje estándar.
Capitán Prinny el
17

Si bien estoy de acuerdo en que es mejor usar la construcción incorporada foreachen la mayoría de los casos, creo que el uso de esta variación en la extensión ForEach <> es un poco mejor que tener que administrar el índice de forma regular foreach:

public static int ForEach<T>(this IEnumerable<T> list, Action<int, T> action)
{
    if (action == null) throw new ArgumentNullException("action");

    var index = 0;

    foreach (var elem in list)
        action(index++, elem);

    return index;
}
Ejemplo
var people = new[] { "Moe", "Curly", "Larry" };
people.ForEach((i, p) => Console.WriteLine("Person #{0} is {1}", i, p));

Te daría:

Person #0 is Moe
Person #1 is Curly
Person #2 is Larry
Chris Zwiryk
fuente
Gran extensión, pero ¿podría agregar alguna documentación? ¿Cuál es el uso previsto del valor devuelto? ¿Por qué es index var en lugar de específicamente un int? Uso de la muestra?
Mark T
Marca: El valor de retorno es el número de elementos en la lista. Index se escribe específicamente como int, gracias a la escritura implícita.
Chris Zwiryk
@Chris, según su código anterior, el valor de retorno es el índice basado en cero del último valor en la lista, no el número de elementos en la lista.
Eric Smith
@Chris, no, no lo es: el índice se incrementa después de que se haya tomado el valor (incremento de posfijo), por lo que después de que se haya procesado el primer elemento, el índice será 1 y así sucesivamente. Entonces el valor de retorno es realmente el recuento de elementos.
MartinStettner
1
@MartinStettner el comentario de Eric Smith el 16/05 fue de una versión anterior. Corrigió el código el 18/05 para devolver el valor correcto.
Michael Blackburn
16

Una solución alternativa es escribir .ToList().ForEach(x => ...).

pros

Fácil de entender: el lector solo necesita saber qué se incluye con C #, no ningún método de extensión adicional.

El ruido sintáctico es muy leve (solo agrega un poco de código extranjero).

Por lo general, no cuesta memoria adicional, ya que un nativo .ForEach()tendría que realizar toda la colección, de todos modos.

contras

El orden de las operaciones no es ideal. Prefiero darme cuenta de un elemento, luego actuar sobre él, luego repetir. Este código realiza primero todos los elementos, luego actúa sobre cada uno de ellos en secuencia.

Si darse cuenta de que la lista arroja una excepción, nunca puede actuar sobre un solo elemento.

Si la enumeración es infinita (como los números naturales), no tienes suerte.

Jay Bazuzi
fuente
1
a) Esto ni siquiera es remotamente una respuesta a la pregunta. b) Esta respuesta sería más clara si mencionara por adelantado que, si bien no existe un Enumerable.ForEach<T>método, sí existe un List<T>.ForEachmétodo. c) "Por lo general, no cuesta memoria adicional, ya que un nativo .ForEach () tendría que realizar toda la colección de todos modos". - Completa y absoluta tontería. Aquí está la implementación de Enumerable.ForEach <T> que C # proporcionaría si lo hiciera:public static void ForEach<T>(this IEnumerable<T> list, Action<T> action) { foreach (T item in list) action(item); }
Jim Balter
13

Siempre me lo he preguntado, es por eso que siempre llevo esto conmigo:

public static void ForEach<T>(this IEnumerable<T> col, Action<T> action)
{
    if (action == null)
    {
        throw new ArgumentNullException("action");
    }
    foreach (var item in col)
    {
        action(item);
    }
}

Pequeño y agradable método de extensión.

Aaron Powell
fuente
2
col podría ser nulo ... podrías comprobar eso también.
Amy B
Sí, verifico en mi biblioteca, lo perdí cuando lo hice rápidamente allí.
Aaron Powell
Me parece interesante que todos comprueben el parámetro de acción para nulo, pero nunca el parámetro fuente / col.
Cameron MacFarland
2
Me parece interesante que todos verifiquen los parámetros como nulos, cuando no
verifiquen
De ningún modo. Una excepción Null Ref no le dirá el nombre del parámetro que tuvo la culpa. Y también puede verificar en el bucle para poder arrojar un Ref. Nulo específico que diga que col [index #] era nulo por la misma razón
Roger Willcocks el
8

Así que ha habido muchos comentarios sobre el hecho de que un método de extensión ForEach no es apropiado porque no devuelve un valor como los métodos de extensión LINQ. Si bien esta es una declaración de hechos, no es del todo cierto.

Todos los métodos de extensión LINQ devuelven un valor para que puedan encadenarse:

collection.Where(i => i.Name = "hello").Select(i => i.FullName);

Sin embargo, el hecho de que LINQ se implemente utilizando métodos de extensión no significa que los métodos de extensión deben usarse de la misma manera y devolver un valor. Escribir un método de extensión para exponer una funcionalidad común que no devuelve un valor es un uso perfectamente válido.

El argumento específico sobre ForEach es que, en función de las restricciones en los métodos de extensión (es decir, que un método de extensión nunca anulará un método heredado con la misma firma ), puede haber una situación en la que el método de extensión personalizado esté disponible en todas las clases que imponen IEnumerable <T> excepto Lista <T>. Esto puede causar confusión cuando los métodos comienzan a comportarse de manera diferente dependiendo de si se llama o no al método de extensión o al método de herencia.

Scott Dorman
fuente
7

Puede usar el (encadenable, pero evaluado perezosamente) Select, primero haciendo su operación y luego devolviendo la identidad (o algo más si lo prefiere)

IEnumerable<string> people = new List<string>(){"alica", "bob", "john", "pete"};
people.Select(p => { Console.WriteLine(p); return p; });

Deberá asegurarse de que todavía se evalúa, ya sea con Count()(la operación más barata para enumerar afaik) u otra operación que necesita de todos modos.

Sin embargo, me encantaría verlo en la biblioteca estándar:

static IEnumerable<T> WithLazySideEffect(this IEnumerable<T> src, Action<T> action) {
  return src.Select(i => { action(i); return i; } );
}

El código anterior se convierte en lo people.WithLazySideEffect(p => Console.WriteLine(p))que es efectivamente equivalente a foreach, pero vago y encadenable.

Martijn
fuente
Fantástico, nunca hizo clic en que una selección devuelta funcionaría así. Un buen uso de la sintaxis del método, ya que no creo que pueda hacer esto con la sintaxis de la expresión (por lo tanto, no lo había pensado así antes).
Alex KeySmith
@AlexKeySmith Podría hacerlo con sintaxis de expresión si C # tuviera un operador de secuencia, como su antecesor. Pero el objetivo de ForEach es que realiza una acción con tipo de vacío, y no puede usar tipos de vacío en expresiones en C #.
Jim Balter
En mi opinión, ahora Selectya no hace lo que se supone que debe hacer. definitivamente es una solución para lo que pide el OP, pero en cuanto a la legibilidad del tema: nadie esperaría que la selección ejecute una función.
Matthias Burger
@MatthiasBurger Si Selectno hace lo que se supone que debe hacer, es un problema con la implementación Select, no con el código que llamaSelect , que no tiene influencia sobre lo que Selecthace.
Martijn
4

@Coincoin

El poder real del método de extensión foreach implica la reutilización de la Action<>sin agregar métodos innecesarios a su código. Digamos que tiene 10 listas y desea realizar la misma lógica en ellas, y una función correspondiente no cabe en su clase y no se reutiliza. En lugar de tener diez para bucles, o una función genérica que obviamente es una ayuda que no pertenece, puede mantener toda su lógica en un solo lugar (el Action<>. Entonces, docenas de líneas se reemplazan con

Action<blah,blah> f = { foo };

List1.ForEach(p => f(p))
List2.ForEach(p => f(p))

etc ...

La lógica está en un lugar y no has contaminado tu clase.

Spoulson
fuente
foreach (var p en List1.Concat (List2) .Concat (List3)) {... do stuff ...}
Cameron MacFarland
Si es tan simple como f(p), a continuación, dejar de lado el { }de la taquigrafía: for (var p in List1.Concat(List2).Concat(List2)) f(p);.
spoulson
3

La mayoría de los métodos de extensión LINQ devuelven resultados. ForEach no se ajusta a este patrón ya que no devuelve nada.

leppie
fuente
2
ForEach utiliza la Acción <T>, que es otra forma de delegado
Aaron Powell el
2
Excepto el derecho de leppie, todos los métodos de extensión Enumerable devuelven un valor, por lo que ForEach no se ajusta al patrón. Los delegados no entran en esto.
Cameron MacFarland
Solo que un método de extensión no tiene que devolver un resultado, sirve para agregar una forma de llamar a una operación de uso frecuente
Aaron Powell
1
De hecho, Slace. Entonces, ¿qué pasa si no devuelve un valor?
TraumaPony
1
@Martijn iepublic IEnumerable<T> Foreach<T>(this IEnumerable<T> items, Action<T> action) { /* error check */ foreach (var item in items) { action(item); yield return item; } }
McKay
3

Si tiene F # (que estará en la próxima versión de .NET), puede usar

Seq.iter doSomething myIEnumerable

TraumaPony
fuente
99
Por lo tanto, Microsoft decidió no proporcionar un método ForEach para un IEnumerable en C # y VB, porque no sería puramente funcional; sin embargo, lo proporcionaron (nombre) en su lenguaje más funcional, F #. Su lógica me alude.
Scott Hutchinson
3

Parcialmente es porque los diseñadores del lenguaje no están de acuerdo con él desde una perspectiva filosófica.

  • No tener (y probar ...) una característica es menos trabajo que tener una característica.
  • No es realmente más corto (hay algunos casos de función de paso donde está, pero ese no sería el uso principal).
  • Su propósito es tener efectos secundarios, que no es de lo que se trata linq.
  • ¿Por qué tener otra forma de hacer lo mismo que una función que ya tenemos? (palabra clave foreach)

https://blogs.msdn.microsoft.com/ericlippert/2009/05/18/foreach-vs-foreach/

McKay
fuente
2

Puedes usar select cuando quieras devolver algo. Si no lo hace, puede usar ToList primero, porque probablemente no quiera modificar nada en la colección.

Paco
fuente
a) Esta no es una respuesta a la pregunta. b) ToList requiere tiempo y memoria extra.
Jim Balter
2

Escribí una publicación de blog al respecto: http://blogs.msdn.com/kirillosenkov/archive/2009/01/31/foreach.aspx

Puede votar aquí si desea ver este método en .NET 4.0: http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=279093

Kirill Osenkov
fuente
¿Se implementó esto alguna vez? Me imagino que no, pero ¿hay alguna otra URL que reemplace ese ticket? MS connect ha sido descontinuado
tocado el
Esto no fue implementado. Considere presentar un nuevo problema en github.com/dotnet/runtime/issues
Kirill Osenkov
2

¿Soy yo o la Lista <T>? Por lo tanto, Linq ha dejado obsoleta. Originalmente hubo

foreach(X x in Y) 

donde Y simplemente tenía que ser IEnumerable (Pre 2.0) e implementar un GetEnumerator (). Si observa el MSIL generado, puede ver que es exactamente igual a

IEnumerator<int> enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
    int i = enumerator.Current;

    Console.WriteLine(i);
}

(Ver http://alski.net/post/0a-for-foreach-forFirst-forLast0a-0a-.aspx para el MSIL)

Luego, en DotNet2.0, aparecieron los genéricos y la lista. Foreach siempre me ha parecido una implementación del patrón Vistor (ver Patrones de diseño de Gamma, Helm, Johnson, Vlissides).

Ahora, por supuesto, en 3.5 podemos usar una Lambda con el mismo efecto, por ejemplo, intente http://dotnet-developments.blogs.techtarget.com/2008/09/02/iterators-lambda-and-linq-oh- mi/

usuario18784
fuente
1
Creo que el código generado para "foreach" debería tener un bloque try-finally. Porque IEnumerator de T hereda de la interfaz IDisposable. Por lo tanto, en el bloque finalmente generado, el IEnumerator de la instancia T debería Disposed.
Morgan Cheng
2

Me gustaría ampliar la respuesta de Aku .

Si desea llamar a un método con el único propósito de su efecto secundario sin iterar todo el enumerable primero, puede usar esto:

private static IEnumerable<T> ForEach<T>(IEnumerable<T> xs, Action<T> f) {
    foreach (var x in xs) {
        f(x); yield return x;
    }
}
fredefox
fuente
1
Esto es lo mismo queSelect(x => { f(x); return x;})
Jim Balter
0

Nadie ha señalado aún que ForEach <T> resulta en la verificación del tipo de tiempo de compilación donde se verifica la palabra clave foreach en tiempo de ejecución.

Después de realizar algunas refactorizaciones donde se utilizaron ambos métodos en el código, estoy a favor de .ForEach, ya que tuve que buscar fallas de prueba / fallas de tiempo de ejecución para encontrar los problemas foreach.

Ardilla
fuente
55
¿Es este realmente el caso? No veo cómo foreachtiene menos tiempo de compilación comprobando. Claro, si su colección es un IEnumerable no genérico, la variable de bucle será un object, pero lo mismo sería cierto para un ForEachmétodo de extensión hipotético .
Richard Poole
1
Esto se menciona en la respuesta aceptada. Sin embargo, simplemente está mal (y Richard Poole arriba es correcto).
Jim Balter