Perché non esiste un metodo di estensione ForEach su IEnumerable?


389

Ispirato da un'altra domanda sulla Zipfunzione mancante :

Perché non esiste un ForEachmetodo di estensione nella Enumerableclasse? O ovunque? L'unica classe che ottiene un ForEachmetodo è List<>. C'è un motivo per cui manca (performance)?


È probabilmente come suggerito da un post precedente, in quanto foreach è supportato come parola chiave in linguaggio C # e VB.NET (e molti altri). La maggior parte delle persone (me compreso) semplicemente ne scrive una se stessa, se necessario e appropriato.
chrisb,

2
Questione connessa qui con il link alla 'risposta ufficiale' stackoverflow.com/questions/858978/...
Benjol


Penso che un punto molto importante sia che un ciclo foreach è più facile da individuare. Se usi .ForEach (...) è facile perdere questo, quando guardi attraverso il codice. Questo diventa importante quando si hanno problemi di prestazioni. Naturalmente .ToList () e .ToArray () hanno lo stesso problema ma sono usati in modo leggermente diverso. Un altro motivo potrebbe essere che è più comune in un .ForEach modificare l'elenco dei sorgenti (ad esempio rimuovendo / aggiungendo elementi) in modo che non sia più una query "pura".
Daniel Bişar,

Quando il debug di codice parallelised, spesso provare a scambiare Parallel.ForEachper Enumerable.ForEachsolo per riscoprire questi ultimi non esiste. C # ha perso un trucco per rendere le cose facili qui.
Colonnello Panic,

Risposte:


198

Esiste già un'istruzione foreach inclusa nella lingua che svolge il lavoro per la maggior parte del tempo.

Non vorrei vedere quanto segue:

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

Invece di:

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

Quest'ultimo è più chiaro e più facile da leggere nella maggior parte delle situazioni , anche se forse è un po 'più lungo da scrivere.

Tuttavia, devo ammettere di aver cambiato la mia posizione su tale questione; un metodo di estensione ForEach () sarebbe effettivamente utile in alcune situazioni.

Ecco le principali differenze tra l'affermazione e il metodo:

  • Controllo del tipo: foreach viene eseguito in fase di esecuzione, ForEach () è in fase di compilazione (Big Plus!)
  • La sintassi per chiamare un delegato è davvero molto più semplice: objects.ForEach (DoSomething);
  • ForEach () potrebbe essere incatenato: sebbene la malvagità / l'utilità di una tale caratteristica sia aperta alla discussione.

Questi sono tutti ottimi punti sollevati da molte persone qui e posso vedere perché alla gente manca la funzione. Non mi dispiacerebbe che Microsoft aggiungesse un metodo ForEach standard nella prossima iterazione del framework.


39
La discussione qui dà la risposta: forums.microsoft.com/MSDN/… Fondamentalmente è stata presa la decisione di mantenere i metodi di estensione funzionalmente "puri". Un ForEach incoraggerebbe gli effetti collaterali quando si utilizzano i metodi di estensione, che non era l'intento.
mancaus,

17
'foreach' può sembrare più chiaro se hai uno sfondo C, ma l'iterazione interna afferma più chiaramente le tue intenzioni. Ciò significa che potresti fare cose come 'sequence.AsParallel (). ForEach (...)'.
Jay Bazuzi,

10
Non sono sicuro che il tuo punto sul controllo del tipo sia corretto. foreachviene controllato in fase di compilazione se l'enumerabile è parametrizzato. Manca solo il controllo del tipo di tempo di compilazione per le raccolte non generiche, così come un ipotetico ForEachmetodo di estensione.
Richard Poole,

49
Metodo di estensione sarebbe davvero utile in catena di LINQ con notazione del punto, come: col.Where(...).OrderBy(...).ForEach(...). Sintassi molto più semplice, nessun inquinamento dello schermo con variabili locali ridondanti o parentesi lunghe in foreach ().
Jacek Gorgoń,

22
"Non vorrei vedere quanto segue" - La domanda non riguardava le tue preferenze personali e hai reso il codice più odioso aggiungendo avanzamenti di riga estranei e un paio di parentesi graffe estranee. Non è così odioso list.ForEach(item => item.DoSomething()). E chi dice che sia un elenco? L'ovvio caso d'uso è alla fine di una catena; con l'istruzione foreach, dovresti inserire l'intera catena ine l'operazione da eseguire all'interno del blocco, che è molto meno naturale o coerente.
Jim Balter,

73

Ogni metodo è stato aggiunto prima di LINQ. Se aggiungi l'estensione ForEach, non verrà mai chiamato per le istanze Elenco a causa dei vincoli dei metodi di estensione. Penso che il motivo per cui non è stato aggiunto sia di non interferire con quello esistente.

Tuttavia, se ti manca davvero questa piccola funzione, puoi implementare la tua versione

public static void ForEach<T>(
    this IEnumerable<T> source,
    Action<T> action)
{
    foreach (T element in source) 
        action(element);
}

11
I metodi di estensione lo consentono. Se esiste già un'implementazione dell'istanza di un determinato nome di metodo, viene utilizzato quel metodo anziché il metodo di estensione. Quindi è possibile implementare un metodo di estensione ForEach e non farlo collidere con il metodo List <>. ForEach.
Cameron MacFarland,

3
Cameron, credimi, so come funzionano i metodi di estensione :) Elenco eredita IEnumerable, quindi sarà una cosa non ovvia.
Aku,

9
Dalla Guida alla programmazione dei metodi di estensione ( msdn.microsoft.com/en-us/library/bb383977.aspx ): non verrà mai chiamato un metodo di estensione con lo stesso nome e la stessa firma di un metodo di interfaccia o classe. Mi sembra abbastanza ovvio.
Cameron MacFarland,

3
Sto parlando di qualcosa di diverso? Penso che non vada bene quando molte classi hanno il metodo ForEach ma alcune potrebbero funzionare in modo diverso.
Aku,

5
Una cosa interessante da notare su List <>. ForEach e Array.ForEach è che non stanno effettivamente usando il costrutto foreach internamente. Entrambi usano un semplice ciclo. Forse è una cosa delle prestazioni ( diditwith.net/2006/10/05/PerformanceOfForeachVsListForEach.aspx ). Detto questo, Array ed List implementano entrambi i metodi ForEach, è sorprendente che almeno non implementassero un metodo di estensione per IList <>, se non anche per IEnumerable <>.
Matt Dotson,

53

È possibile scrivere questo metodo di estensione:

// 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;
    }
}

Professionisti

Permette il concatenamento:

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

Contro

In realtà non farà nulla finché non fai qualcosa per forzare l'iterazione. Per questo motivo, non dovrebbe essere chiamato .ForEach(). Potresti scrivere .ToList()alla fine o puoi scrivere anche questo metodo di estensione:

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

    return source;
}

Questo potrebbe essere un allontanamento troppo significativo dalle librerie C # di spedizione; i lettori che non hanno familiarità con i metodi di estensione non sanno cosa fare del codice.


1
La tua prima funzione potrebbe essere usata senza il metodo 'realizzo' se scritta come:{foreach (var e in source) {action(e);} return source;}
jjnguy

3
Non potresti semplicemente usare Seleziona invece del tuo metodo Apply? Mi sembra che applicare un'azione a ciascun elemento e produrlo sia esattamente ciò che fa Select. Ad esempionumbers.Select(n => n*2);
rasmusvhansen,

1
Penso che Apply sia più leggibile dell'uso di Select a questo scopo. Durante la lettura di un'istruzione Select, l'ho letta come "ottieni qualcosa dall'interno", dove come si applica "fai un po 'di lavoro".
Dott. Rob Lang,

2
@rasmusvhansen In realtà, Select()dice applica una funzione a ciascun elemento e restituisce il valore della funzione dove Apply()dice applica un Actiona ciascun elemento e restituisce l' elemento originale .
NetMage,

1
Ciò equivale aSelect(e => { action(e); return e; })
Jim Balter,

35

La discussione qui dà la risposta:

In realtà, la discussione specifica a cui ho assistito in effetti dipendeva dalla purezza funzionale. In un'espressione, ci sono spesso ipotesi fatte sul non avere effetti collaterali. Avere ForEach invita in modo specifico gli effetti collaterali piuttosto che tollerarli. - Keith Farmer (Partner)

Fondamentalmente è stata presa la decisione di mantenere i metodi di estensione funzionalmente "puri". Un ForEach incoraggerebbe effetti collaterali quando si utilizzano i metodi di estensione Enumerable, che non era l'intento.


6
Vedi anche blogs.msdn.com/b/ericlippert/archive/2009/05/18/… . Ddoing viola così i principi di programmazione funzionale su cui si basano tutti gli altri operatori di sequenza. Chiaramente l'unico scopo di una chiamata a questo metodo è quello di causare effetti collaterali
Michael Freidgeim,

7
Questo ragionamento è completamente falso. Il caso d'uso è che sono necessari effetti collaterali ... senza ForEach, devi scrivere un ciclo foreach. L'esistenza di ForEach non incoraggia gli effetti collaterali e la sua assenza non li riduce.
Jim Balter,

Considerato quanto sia semplice scrivere il metodo di estensione, non c'è letteralmente alcun motivo per cui non è un linguaggio standard.
Capitano Prinny,

17

Mentre sono d'accordo che foreachnella maggior parte dei casi è meglio usare il costrutto integrato , trovo che l'uso di questa variazione sull'estensione ForEach <> sia un po 'più bello che dover gestire l'indice in modo regolare 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;
}
Esempio
var people = new[] { "Moe", "Curly", "Larry" };
people.ForEach((i, p) => Console.WriteLine("Person #{0} is {1}", i, p));

Ti darebbe:

Person #0 is Moe
Person #1 is Curly
Person #2 is Larry

Grande estensione, ma potresti aggiungere della documentazione? Qual è l'uso previsto del valore restituito? Perché index var piuttosto che specificamente un int? Utilizzo del campione?
Mark T

Mark: il valore restituito è il numero di elementi nell'elenco. L'indice viene digitato in modo specifico come int, grazie alla digitazione implicita.
Chris Zwiryk,

@Chris, in base al codice sopra riportato, il valore restituito è l'indice in base zero dell'ultimo valore nell'elenco, non il numero di elementi nell'elenco.
Eric Smith,

@Chris, no non lo è: l'indice viene incrementato dopo che il valore è stato acquisito (incremento postfisso), quindi dopo l'elaborazione del primo elemento l'indice sarà 1 e così via. Quindi il valore restituito è davvero il conteggio degli elementi.
MartinStettner,

1
@MartinStettner il commento di Eric Smith del 16/5 era di una versione precedente. Ha corretto il codice il 5/18 per restituire il valore corretto.
Michael Blackburn,

16

Una soluzione alternativa è scrivere .ToList().ForEach(x => ...) .

professionisti

Facile da capire: il lettore deve solo sapere cosa viene fornito con C #, non con altri metodi di estensione.

Il rumore sintattico è molto lieve (aggiunge solo un po 'di codice extranious).

Di solito non costa memoria aggiuntiva, poiché nativo .ForEach() dovrebbe comunque realizzare l'intera collezione.

cons

L'ordine delle operazioni non è l'ideale. Preferirei realizzare un elemento, quindi agire su di esso, quindi ripetere. Questo codice realizza prima tutti gli elementi, quindi agisce su ciascuno di essi in sequenza.

Se la realizzazione dell'elenco genera un'eccezione, non potrai mai agire su un singolo elemento.

Se l'enumerazione è infinita (come i numeri naturali), sei sfortunato.


1
a) Questa non è nemmeno una risposta remota alla domanda. b) Questa risposta sarebbe più chiara se menzionasse in anticipo che, mentre non esiste un Enumerable.ForEach<T>metodo, esiste un List<T>.ForEachmetodo. c) "Di solito non costa più memoria, dal momento che un nativo .ForEach () dovrebbe comunque realizzare l'intera collezione." - completa e totale assurdità. Ecco l'implementazione di Enumerable.ForEach <T> che C # fornirebbe se lo facesse:public static void ForEach<T>(this IEnumerable<T> list, Action<T> action) { foreach (T item in list) action(item); }
Jim Balter,

13

Mi sono sempre chiesto che io stesso, ecco perché lo porto sempre con me:

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

Bel metodo di estensione.


2
col potrebbe essere nullo ... anche tu puoi verificarlo.
Amy B,

Sì, faccio il check-in nella mia libreria, l'ho perso quando l'ho fatto rapidamente lì.
Aaron Powell,

Trovo interessante che tutti controllino il parametro action per null, ma mai il parametro source / col.
Cameron MacFarland

2
Trovo interessante che tutti controllino i parametri per null, quando non controllano i risultati anche in un'eccezione ...
oɔɯǝɹ

Affatto. Un'eccezione Null Ref non ti direbbe il nome del parametro che era in errore. E potresti anche controllare nel loop in modo da poter lanciare un riferimento Null specifico dicendo che col [indice #] era nullo per lo stesso motivo
Roger Willcocks,

8

Quindi ci sono stati molti commenti sul fatto che un metodo di estensione ForEach non è appropriato perché non restituisce un valore come i metodi di estensione LINQ. Mentre questa è un'affermazione di fatto, non è del tutto vera.

I metodi di estensione LINQ restituiscono tutti un valore in modo che possano essere concatenati:

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

Tuttavia, solo perché LINQ è implementato utilizzando i metodi di estensione non significa che i metodi di estensione debbano essere utilizzati allo stesso modo e restituire un valore. Scrivere un metodo di estensione per esporre funzionalità comuni che non restituiscono un valore è un uso perfettamente valido.

L'argomentazione specifica su ForEach è che, in base ai vincoli sui metodi di estensione (ovvero che un metodo di estensione non sostituirà mai un metodo ereditato con la stessa firma ), potrebbe esserci una situazione in cui il metodo di estensione personalizzato è disponibile su tutte le classi che impelement IEnumerable <T> tranne Elenco <T>. Ciò può causare confusione quando i metodi iniziano a comportarsi in modo diverso a seconda che venga chiamato o meno il metodo di estensione o il metodo di ereditarietà.


7

Puoi usare (concatenabile, ma valutato pigramente) Select, prima facendo la tua operazione e poi restituendo l'identità (o qualcos'altro se preferisci)

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

Dovrai assicurarti che sia ancora valutato, con Count()(l'operazione più economica per enumerare afaik) o con un'altra operazione che ti serviva comunque.

Mi piacerebbe vederlo portato nella libreria standard però:

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

Il codice sopra diventa quindi people.WithLazySideEffect(p => Console.WriteLine(p))che è effettivamente equivalente a foreach, ma pigro e concatenabile.


Fantastico, non ha mai fatto clic che una selezione restituita avrebbe funzionato in questo modo. Un buon uso della sintassi del metodo, poiché non credo che si possa fare questo con la sintassi dell'espressione (quindi non ci ho pensato prima).
Alex KeySmith,

@AlexKeySmith Potresti farlo con la sintassi delle espressioni se C # avesse un operatore di sequenza, come il suo antenato. Ma il punto centrale di ForEach è che esegue un'azione con il tipo vuoto e non è possibile utilizzare i tipi vuoto nelle espressioni in C #.
Jim Balter,

imho, ora Selectnon fa più quello che dovrebbe fare. è sicuramente una soluzione per ciò che l'OP chiede, ma per la leggibilità degli argomenti: nessuno si aspetterebbe che la selezione esegua una funzione.
Matthias Burger,

@MatthiasBurger Se Selectnon fa quello che dovrebbe fare, questo è un problema con l'implementazione Select, non con il codice che chiama Select, che non ha alcuna influenza su ciò che Selectfa.
Martijn,


4

@Coincoin

Il vero potere del metodo di estensione foreach comporta la riusabilità di Action<>senza aggiungere metodi non necessari al codice. Supponi di avere 10 elenchi e desideri eseguire la stessa logica su di essi e che una funzione corrispondente non rientri nella tua classe e non venga riutilizzata. Invece di avere dieci per loop, o una funzione generica che è ovviamente un aiuto che non appartiene, puoi mantenere tutta la tua logica in un posto (il Action<>. Quindi, dozzine di linee vengono sostituite con

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

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

eccetera...

La logica è in un posto e non hai inquinato la tua classe.


foreach (var p in List1.Concat (List2) .Concat (List3)) {... do stuff ...}
Cameron MacFarland

Se è così semplice come f(p), poi lasciare la { }per la stenografia: for (var p in List1.Concat(List2).Concat(List2)) f(p);.
spoulson,

3

La maggior parte dei metodi di estensione LINQ restituisce risultati. Ciascuno non si adatta a questo modello in quanto non restituisce nulla.


2
ForEach utilizza Action <T>, che è un'altra forma di delegato
Aaron Powell,

2
Tranne il diritto di leppie, tutti i metodi di estensione Enumerable restituiscono un valore, quindi ForEach non si adatta al modello. I delegati non entrano in questo.
Cameron MacFarland,

Solo che un metodo di estensione non deve restituire un risultato, serve allo scopo di aggiungere un modo per chiamare un'operazione usata di frequente
Aaron Powell,

1
Anzi, Slace. E se non restituisse un valore?
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

Se hai F # (che sarà nella prossima versione di .NET), puoi usare

Seq.iter doSomething myIEnumerable


9
Quindi Microsoft ha scelto di non fornire un metodo ForEach per un IEnumerable in C # e VB, perché non sarebbe stato puramente funzionale; eppure l'hanno fornito (nome iter) nel loro linguaggio più funzionale, F #. La loro logica mi allude.
Scott Hutchinson,

3

In parte è perché i progettisti del linguaggio non sono d'accordo con esso da una prospettiva filosofica.

  • Non avere (e testare ...) una funzione è meno lavoro che avere una funzione.
  • Non è molto più breve (ci sono alcuni casi di funzioni di passaggio in cui si trova, ma non sarebbe l'uso primario).
  • Lo scopo è quello di avere effetti collaterali, che non è ciò di cui si occupa linq.
  • Perché un altro modo di fare la stessa cosa di una funzione che abbiamo già? (parola chiave foreach)

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


2

Puoi usare select quando vuoi restituire qualcosa. In caso contrario, è possibile utilizzare prima ToList, perché probabilmente non si desidera modificare nulla nella raccolta.


a) Questa non è una risposta alla domanda. b) ToList richiede tempo e memoria extra.
Jim Balter,

2

Ho scritto un post sul blog a riguardo: http://blogs.msdn.com/kirillosenkov/archive/2009/01/31/foreach.aspx

Puoi votare qui se desideri vedere questo metodo in .NET 4.0: http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=279093


questo è mai stato implementato? Immagino di no, ma c'è qualche altro URL che sostituisce quel biglietto? MS connect è stato sospeso
knocte il

Questo non è stato implementato. Prendi in considerazione la possibilità di presentare un nuovo numero su github.com/dotnet/runtime/issues
Kirill Osenkov


2

Sono io o è la lista <T>. La consegna è stata praticamente resa obsoleta da Linq. Inizialmente c'era

foreach(X x in Y) 

dove Y doveva semplicemente essere IEnumerable (Pre 2.0) e implementare un GetEnumerator (). Se guardi il MSIL generato, puoi vedere che è esattamente lo stesso di

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

    Console.WriteLine(i);
}

(Vedi http://alski.net/post/0a-for-foreach-forFirst-forLast0a-0a-.aspx per MSIL)

Quindi in DotNet2.0 sono arrivati ​​i generici e l'elenco. Foreach mi è sempre sembrato un'implementazione del modello Vistor (vedi Design Patterns di Gamma, Helm, Johnson, Vlissides).

Ora ovviamente nella 3.5 possiamo invece usare una Lambda con lo stesso effetto, per esempio provare http://dotnet-developments.blogs.techtarget.com/2008/09/02/iterators-lambda-and-linq-oh- mio/


1
Credo che il codice generato per "foreach" dovrebbe avere un blocco try-finally. Perché IEnumerator di T eredita dall'interfaccia IDisposable. Quindi, nel blocco generato infine, l'IEnumerator dell'istanza T dovrebbe essere eliminato.
Morgan Cheng,

2

Vorrei ampliare la risposta di Aku .

Se vuoi chiamare un metodo al solo scopo del suo effetto collaterale senza ripetere prima l'intero elenco enumerabile, puoi usare questo:

private static IEnumerable<T> ForEach<T>(IEnumerable<T> xs, Action<T> f) {
    foreach (var x in xs) {
        f(x); yield return x;
    }
}

1
È lo stesso diSelect(x => { f(x); return x;})
Jim Balter,

0

Nessuno ha ancora sottolineato che ForEach <T> si traduce in un controllo del tipo di tempo di compilazione in cui viene verificata la runtime della parola chiave foreach.

Dopo aver effettuato alcuni refactoring in cui entrambi i metodi sono stati utilizzati nel codice, preferisco. Per ogni motivo, poiché ho dovuto dare la caccia agli errori di test / errori di runtime per trovare i problemi di foreach.


5
È davvero così? Non riesco a vedere in che modo il foreachcontrollo del tempo di compilazione è inferiore. Certo, se la tua raccolta è un IEnumerable non generico, la variabile loop sarà un object, ma lo stesso sarebbe vero per un ipotetico ForEachmetodo di estensione.
Richard Poole,

1
Questo è menzionato nella risposta accettata. Tuttavia, è semplicemente sbagliato (e Richard Poole sopra è corretto).
Jim Balter,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.