Pourquoi il n'y a pas de méthode d'extension ForEach sur IEnumerable?

389

Inspiré par une autre question concernant la Zipfonction manquante :

Pourquoi n'y a-t-il pas de ForEachméthode d'extension dans la Enumerableclasse? Ou n'importe où? La seule classe qui obtient une ForEachméthode est List<>. Y a-t-il une raison pour laquelle il manque (performances)?

Cameron MacFarland
la source
Son probablement comme laissé entendre par un post précédent, dans la mesure où foreach est pris en charge en tant que mot-clé de langage en C # et VB.NET (et bien d'autres) La plupart des gens (moi y compris) écrivent simplement un eux-mêmes si nécessaire et approprié.
chrisb
2
Question connexe ici avec un lien vers la «réponse officielle» stackoverflow.com/questions/858978/…
Benjol
Je pense qu'un point très important est qu'une boucle foreach est plus facile à repérer. Si vous utilisez .ForEach (...), il est facile de manquer celui-ci, lorsque vous parcourez le code. Cela devient important lorsque vous rencontrez des problèmes de performances. Bien sûr .ToList () et .ToArray () ont le même problème mais ils sont utilisés un peu différemment. Une autre raison pourrait être qu'il est plus courant dans un .ForEach de modifier la liste source (par exemple, supprimer / ajouter des éléments) afin que ce ne soit plus une requête "pure".
Daniel Bişar
Lorsque le débogage du code parallélisé, je tente souvent échange Parallel.ForEachpour Enumerable.ForEachque de retrouver ce dernier n'existe pas. C # a raté une astuce pour rendre les choses faciles ici.
Colonel Panic

Réponses:

198

Il y a déjà une déclaration foreach incluse dans la langue qui fait le travail la plupart du temps.

Je détesterais voir ce qui suit:

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

Au lieu de:

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

Ce dernier est plus clair et plus facile à lire dans la plupart des situations , mais peut-être un peu plus long à taper.

Cependant, je dois admettre que j'ai changé de position sur cette question; une méthode d'extension ForEach () serait en effet utile dans certaines situations.

Voici les principales différences entre l'énoncé et la méthode:

  • Vérification de type: foreach se fait au moment de l'exécution, ForEach () est au moment de la compilation (Big Plus!)
  • La syntaxe pour appeler un délégué est en effet beaucoup plus simple: objects.ForEach (DoSomething);
  • ForEach () pourrait être enchaîné: bien que la méchanceté / l'utilité d'une telle fonctionnalité soit sujette à discussion.

Ce sont tous d'excellents points soulevés par de nombreuses personnes ici et je peux voir pourquoi les gens manquent la fonction. Cela ne me dérangerait pas que Microsoft ajoute une méthode ForEach standard dans la prochaine itération du framework.

Coincoin
la source
39
La discussion ici donne la réponse: forums.microsoft.com/MSDN/… Fondamentalement, la décision a été prise de garder les méthodes d'extension fonctionnellement «pures». Un ForEach encouragerait les effets secondaires lors de l'utilisation des méthodes d'extension, ce qui n'était pas l'intention.
mancaus
17
'foreach' peut sembler plus clair si vous avez un arrière-plan C, mais l'itération interne indique plus clairement votre intention. Cela signifie que vous pouvez faire des choses comme 'sequence.AsParallel (). ForEach (...)'.
Jay Bazuzi
10
Je ne suis pas sûr que votre point sur la vérification de type soit correct. foreachest le type vérifié au moment de la compilation si l'énumérable est paramétré. Il ne manque que la vérification du type de temps de compilation pour les collections non génériques, comme le ferait une ForEachméthode d'extension hypothétique .
Richard Poole
49
Méthode d'extension serait vraiment utile dans la chaîne LINQ avec la notation par points, comme: col.Where(...).OrderBy(...).ForEach(...). Syntaxe beaucoup plus simple, pas de pollution d'écran avec des variables locales redondantes ou des crochets longs dans foreach ().
Jacek Gorgoń
22
"Je détesterais voir ce qui suit" - La question ne concernait pas vos préférences personnelles, et vous avez rendu le code plus haineux en ajoutant des sauts de ligne superflus et une paire de croisillons superflus. Pas si détestable list.ForEach(item => item.DoSomething()). Et qui dit que c'est une liste? Le cas d'utilisation évident est au bout d'une chaîne; avec l'instruction foreach, vous devrez mettre toute la chaîne après in, et l'opération à effectuer à l'intérieur du bloc, ce qui est beaucoup moins naturel ou cohérent.
Jim Balter
73

La méthode ForEach a été ajoutée avant LINQ. Si vous ajoutez l'extension ForEach, elle ne sera jamais appelée pour les instances List en raison des contraintes des méthodes d'extension. Je pense que la raison pour laquelle il n'a pas été ajouté est de ne pas interférer avec celui qui existe déjà.

Cependant, si vous manquez vraiment cette petite fonction sympa, vous pouvez déployer votre propre version

public static void ForEach<T>(
    this IEnumerable<T> source,
    Action<T> action)
{
    foreach (T element in source) 
        action(element);
}
aku
la source
11
Les méthodes d'extension le permettent. S'il existe déjà une implémentation d'instance d'un nom de méthode donné, cette méthode est utilisée à la place de la méthode d'extension. Vous pouvez donc implémenter une méthode d'extension ForEach et ne pas la faire entrer en collision avec la méthode List <>. ForEach.
Cameron MacFarland,
3
Cameron, croyez-moi, je sais comment fonctionnent les méthodes d'extension :) La liste hérite d'IEnumerable, ce sera donc une chose non évidente.
aku le
9
À partir du Guide de programmation des méthodes d'extension ( msdn.microsoft.com/en-us/library/bb383977.aspx ): Une méthode d'extension avec le même nom et la même signature qu'une interface ou une méthode de classe ne sera jamais appelée. Cela me semble assez évident.
Cameron MacFarland
3
Suis-je en train de parler de quelque chose de différent? Je pense que ce n'est pas bon lorsque de nombreuses classes ont la méthode ForEach mais certaines peuvent fonctionner différemment.
aku
5
Une chose intéressante à noter à propos de List <>. ForEach et Array.ForEach est qu'ils n'utilisent pas réellement la construction foreach en interne. Ils utilisent tous les deux une boucle for for. C'est peut-être une question de performance ( diditwith.net/2006/10/05/PerformanceOfForeachVsListForEach.aspx ). Mais étant donné que, Array et List implémentent tous deux des méthodes ForEach, il est surprenant qu'ils n'aient pas au moins implémenté une méthode d'extension pour IList <>, sinon pour IEnumerable <>.
Matt Dotson
53

Vous pouvez écrire cette méthode d'extension:

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

Avantages

Permet le chaînage:

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

Les inconvénients

Il ne fera rien tant que vous n'aurez pas forcé l'itération. Pour cette raison, il ne faut pas l'appeler .ForEach(). Vous pouvez écrire .ToList()à la fin, ou vous pouvez également écrire cette méthode d'extension:

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

    return source;
}

Cela peut être un écart trop important par rapport aux bibliothèques C # d'expédition; les lecteurs qui ne connaissent pas vos méthodes d'extension ne sauront pas quoi faire de votre code.

Jay Bazuzi
la source
1
Votre première fonction pourrait être utilisée sans la méthode «réaliser» si elle était écrite comme:{foreach (var e in source) {action(e);} return source;}
jjnguy
3
Ne pourriez-vous pas simplement utiliser Select au lieu de votre méthode Apply? Il me semble qu'appliquer une action à chaque élément et la produire est exactement ce que fait Select. Par exemplenumbers.Select(n => n*2);
rasmusvhansen
1
Je pense qu'Appliquer est plus lisible que d'utiliser Select à cette fin. Lors de la lecture d'une instruction Select, je la lis comme «obtenir quelque chose de l'intérieur», alors que Apply signifie «faire un peu de travail».
Dr Rob Lang
2
@rasmusvhansen En fait, Select()dit appliquer une fonction à chaque élément et retourner la valeur de la fonction où Apply()dit appliquer un Actionà chaque élément et retourner l' élément d' origine .
NetMage
1
Cela équivaut àSelect(e => { action(e); return e; })
Jim Balter
35

La discussion ici donne la réponse:

En fait, la discussion spécifique dont j'ai été témoin reposait en fait sur la pureté fonctionnelle. Dans une expression, il y a souvent des hypothèses faites pour ne pas avoir d'effets secondaires. Avoir ForEach est spécifiquement une invitation aux effets secondaires plutôt que de simplement les supporter. - Keith Farmer (associé)

Fondamentalement, la décision a été prise de garder les méthodes d'extension fonctionnellement «pures». Un ForEach encouragerait les effets secondaires lors de l'utilisation des méthodes d'extension Enumerable, ce qui n'était pas l'intention.

mancaus
la source
6
Voir aussi blogs.msdn.com/b/ericlippert/archive/2009/05/18/… . Ddoing viole ainsi les principes de programmation fonctionnelle sur lesquels tous les autres opérateurs de séquence sont basés. De toute évidence, le seul but d'un appel à cette méthode est de provoquer des effets secondaires
Michael Freidgeim
7
Ce raisonnement est complètement faux. Le cas d'utilisation est que des effets secondaires sont nécessaires ... sans ForEach, vous devez écrire une boucle foreach. L'existence de ForEach n'encourage pas les effets secondaires et son absence ne les réduit pas.
Jim Balter
Étant donné la simplicité d'écriture de la méthode d'extension, il n'y a littéralement aucune raison que ce ne soit pas un langage standard.
Captain Prinny
17

Bien que je convienne qu'il est préférable d'utiliser la construction intégrée foreachdans la plupart des cas, je trouve que l'utilisation de cette variation sur l'extension ForEach <> est un peu plus agréable que d'avoir à gérer foreachmoi-même l'index de manière régulière :

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;
}
Exemple
var people = new[] { "Moe", "Curly", "Larry" };
people.ForEach((i, p) => Console.WriteLine("Person #{0} is {1}", i, p));

Vous donnerait:

Person #0 is Moe
Person #1 is Curly
Person #2 is Larry
Chris Zwiryk
la source
Grande extension, mais pourriez-vous ajouter de la documentation? Quelle est l'utilisation prévue de la valeur retournée? Pourquoi l'index var plutôt que spécifiquement un int? Exemple d'utilisation?
Mark T
Mark: La valeur de retour est le nombre d'éléments dans la liste. L'index est spécifiquement typé en tant qu'int, grâce au typage implicite.
Chris Zwiryk
@Chris, en fonction de votre code ci-dessus, la valeur de retour est l'index de base zéro de la dernière valeur de la liste, pas le nombre d'éléments dans la liste.
Eric Smith,
@Chris, non ce n'est pas le cas: l'index est incrémenté après que la valeur a été prise (incrément postfix), donc après le premier élément a été traité, l'index sera 1 et ainsi de suite. Donc, la valeur de retour est vraiment le nombre d'éléments.
MartinStettner
1
@MartinStettner le commentaire d'Eric Smith le 5/16 provenait d'une version antérieure. Il a corrigé le code le 5/18 pour renvoyer la valeur correcte.
Michael Blackburn
16

Une solution consiste à écrire .ToList().ForEach(x => ...).

avantages

Facile à comprendre - le lecteur a seulement besoin de savoir ce qui est livré avec C #, pas de méthodes d'extension supplémentaires.

Le bruit syntaxique est très doux (ajoute seulement un peu de code extranious).

Cela ne coûte généralement pas de mémoire supplémentaire, car un natif .ForEach()devrait de toute façon réaliser la collection entière.

les inconvénients

L'ordre des opérations n'est pas idéal. Je préfère réaliser un élément, puis agir en conséquence, puis répéter. Ce code réalise d'abord tous les éléments, puis agit sur chacun d'eux en séquence.

Si la réalisation de la liste lève une exception, vous ne pouvez jamais agir sur un seul élément.

Si l'énumération est infinie (comme les nombres naturels), vous n'avez pas de chance.

Jay Bazuzi
la source
1
a) Ce n'est même pas une réponse à distance à la question. b) Cette réponse serait plus claire si elle mentionnait d'emblée que, s'il n'y a pas de Enumerable.ForEach<T>méthode, il y en a une List<T>.ForEach. c) "Ne coûte généralement pas de mémoire supplémentaire, car un .ForEach () natif devrait de toute façon réaliser la collection entière." - un non-sens complet et absolu. Voici l'implémentation d'Enumerable.ForEach <T> que C # fournirait si c'était le cas:public static void ForEach<T>(this IEnumerable<T> list, Action<T> action) { foreach (T item in list) action(item); }
Jim Balter
13

Je me suis toujours demandé cela, c'est pourquoi je porte toujours ça avec moi:

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

Belle petite méthode d'extension.

Aaron Powell
la source
2
col pourrait être nul ... vous pouvez également vérifier cela.
Amy B
Ouais je vérifie dans ma bibliothèque, je l'ai juste raté en le faisant rapidement là-bas.
Aaron Powell,
Je trouve intéressant que tout le monde vérifie le paramètre action pour null, mais jamais le paramètre source / col.
Cameron MacFarland
2
Je trouve intéressant que tout le monde vérifie les paramètres pour null, alors qu'il ne vérifie pas les résultats dans une exception aussi ...
oɔɯǝɹ
Pas du tout. Une exception Null Ref ne vous indiquerait pas le nom du paramètre en cause. Et vous pouvez également vérifier dans la boucle afin que vous puissiez lancer une référence Null spécifique indiquant que col [index #] était nul pour la même raison
Roger Willcocks
8

Il y a donc eu beaucoup de commentaires sur le fait qu'une méthode d'extension ForEach n'est pas appropriée car elle ne retourne pas une valeur comme les méthodes d'extension LINQ. Bien qu'il s'agisse d'une déclaration factuelle, ce n'est pas entièrement vrai.

Les méthodes d'extension LINQ renvoient toutes une valeur afin qu'elles puissent être chaînées ensemble:

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

Cependant, ce n'est pas parce que LINQ est implémenté à l'aide de méthodes d'extension que les méthodes d'extension doivent être utilisées de la même manière et renvoyer une valeur. Écrire une méthode d'extension pour exposer une fonctionnalité commune qui ne renvoie pas de valeur est une utilisation parfaitement valide.

L'argument spécifique à propos de ForEach est que, sur la base des contraintes sur les méthodes d'extension (à savoir qu'une méthode d'extension ne remplacera jamais une méthode héritée avec la même signature ), il peut y avoir une situation où la méthode d'extension personnalisée est disponible sur toutes les classes qui se propulsent IEnumerable <T> sauf List <T>. Cela peut être source de confusion lorsque les méthodes commencent à se comporter différemment selon que la méthode d'extension ou la méthode héritée est appelée ou non.

Scott Dorman
la source
7

Vous pouvez utiliser le (chaînable, mais évalué paresseusement) Select, d'abord faire votre opération, puis renvoyer l'identité (ou autre chose si vous préférez)

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

Vous devrez vous assurer qu'il est toujours évalué, soit avec Count()(l'opération la moins chère pour énumérer afaik) soit avec une autre opération dont vous aviez besoin de toute façon.

Je serais ravi de le voir intégré à la bibliothèque standard:

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

Le code ci-dessus devient alors people.WithLazySideEffect(p => Console.WriteLine(p))ce qui est effectivement équivalent à foreach, mais paresseux et chaînable.

Martijn
la source
Fantastique, il n'a jamais cliqué qu'une sélection retournée fonctionnerait comme ça. Une belle utilisation de la syntaxe de la méthode, car je ne pense pas que vous puissiez faire cela avec la syntaxe d'expression (donc je n'y ai pas pensé auparavant).
Alex KeySmith
@AlexKeySmith Vous pourriez le faire avec la syntaxe d'expression si C # avait un opérateur de séquence, comme son ancêtre. Mais le but de ForEach est qu'il prend une action avec le type void, et vous ne pouvez pas utiliser les types void dans les expressions en C #.
Jim Balter
à mon humble avis, maintenant le Selectne fait plus ce qu'il est censé faire. c'est certainement une solution pour ce que l'OP demande - mais sur la lisibilité du sujet: personne ne s'attendrait à ce que le select exécute une fonction.
Matthias Burger
@MatthiasBurger If Selectne fait pas ce qu'il est censé faire, c'est un problème avec l'implémentation de Select, pas avec le code qui appelle Select, qui n'a aucune influence sur ce qui le Selectfait.
Martijn
4

@Coincoin

La véritable puissance de la méthode d'extension foreach implique la réutilisation de la Action<>sans ajouter des méthodes inutiles à votre code. Supposons que vous ayez 10 listes et que vous souhaitiez effectuer la même logique sur elles, et qu'une fonction correspondante ne rentre pas dans votre classe et n'est pas réutilisée. Au lieu d'avoir dix pour les boucles, ou une fonction générique qui est évidemment une aide qui n'appartient pas, vous pouvez garder toute votre logique en un seul endroit (le Action<>. Donc, des dizaines de lignes sont remplacées par

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

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

etc...

La logique est au même endroit et vous n'avez pas pollué votre classe.

spoulson
la source
foreach (var p dans List1.Concat (List2) .Concat (List3)) {... faire des trucs ...}
Cameron MacFarland
Si elle est aussi simple que f(p), laissez alors la { }pour un raccourci: for (var p in List1.Concat(List2).Concat(List2)) f(p);.
spoulson
3

La plupart des méthodes d'extension LINQ renvoient des résultats. ForEach ne rentre pas dans ce modèle car il ne renvoie rien.

leppie
la source
2
ForEach utilise Action <T>, qui est une autre forme de délégué
Aaron Powell
2
À l'exception de leppie's right, toutes les méthodes d'extension Enumerable renvoient une valeur, donc ForEach ne correspond pas au modèle. Les délégués n'entrent pas dans le détail.
Cameron MacFarland,
Juste qu'une méthode d'extension n'a pas à retourner un résultat, elle sert à ajouter un moyen d'appeler une opération fréquemment utilisée
Aaron Powell
1
En effet, Slace. Et si elle ne renvoie pas de valeur?
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 vous avez F # (qui sera dans la prochaine version de .NET), vous pouvez utiliser

Seq.iter doSomething myIEnumerable

TraumaPony
la source
9
Microsoft a donc choisi de ne pas fournir de méthode ForEach pour un IEnumerable en C # et VB, car elle ne serait pas purement fonctionnelle; pourtant, ils l'ont fait (nom iter) dans leur langage plus fonctionnel, F #. Leur logique me fait allusion.
Scott Hutchinson
3

C'est en partie parce que les concepteurs de langage ne sont pas d'accord avec cela d'un point de vue philosophique.

  • Ne pas avoir (et tester ...) une fonctionnalité est moins de travail que d'avoir une fonctionnalité.
  • Ce n'est pas vraiment plus court (il y a des cas de fonction qui passent, mais ce ne serait pas l'utilisation principale).
  • Son but est d'avoir des effets secondaires, ce qui n'est pas le but de linq.
  • Pourquoi avoir une autre façon de faire la même chose qu'une fonctionnalité que nous avons déjà? (pour chaque mot clé)

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

McKay
la source
2

Vous pouvez utiliser select lorsque vous souhaitez retourner quelque chose. Si vous ne le faites pas, vous pouvez d'abord utiliser ToList, car vous ne voulez probablement pas modifier quoi que ce soit dans la collection.

Paco
la source
a) Ce n'est pas une réponse à la question. b) ToList nécessite plus de temps et de mémoire.
Jim Balter
2

J'ai écrit un blog à ce sujet: http://blogs.msdn.com/kirillosenkov/archive/2009/01/31/foreach.aspx

Vous pouvez voter ici si vous souhaitez voir cette méthode dans .NET 4.0: http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=279093

Kirill Osenkov
la source
cela a-t-il déjà été mis en œuvre? J'imagine que non, mais y a-t-il une autre URL qui remplace ce ticket? MS connect n'est plus disponible
knocte
Cela n'a pas été mis en œuvre. Envisagez de déposer un nouveau problème sur github.com/dotnet/runtime/issues
Kirill Osenkov
2

Est-ce moi ou la liste <T> .Foreach a été rendu presque obsolète par Linq. Il y avait à l'origine

foreach(X x in Y) 

où Y devait simplement être IEnumerable (Pre 2.0), et implémenter un GetEnumerator (). Si vous regardez le MSIL généré, vous pouvez voir qu'il est exactement le même que

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

    Console.WriteLine(i);
}

(Voir http://alski.net/post/0a-for-foreach-forFirst-forLast0a-0a-.aspx pour le MSIL)

Puis, dans DotNet2.0, les génériques sont arrivés et la liste. Foreach m'a toujours semblé être une implémentation du modèle Vistor (voir Design Patterns by Gamma, Helm, Johnson, Vlissides).

Maintenant, bien sûr, en 3.5, nous pouvons plutôt utiliser un Lambda dans le même sens, par exemple, essayez http://dotnet-developments.blogs.techtarget.com/2008/09/02/iterators-lambda-and-linq-oh- mon/

user18784
la source
1
Je crois que le code généré pour "foreach" devrait avoir un bloc try-finally. Parce que IEnumerator de T hérite de l'interface IDisposable. Ainsi, dans le bloc finalement généré, l'instance IEnumerator de T doit Disposed.
Morgan Cheng
2

Je voudrais développer la réponse d'Aku .

Si vous voulez appeler une méthode dans le seul but de son effet secondaire sans itérer tout l'énumération d'abord, vous pouvez utiliser ceci:

private static IEnumerable<T> ForEach<T>(IEnumerable<T> xs, Action<T> f) {
    foreach (var x in xs) {
        f(x); yield return x;
    }
}
fredefox
la source
1
C'est la même chose queSelect(x => { f(x); return x;})
Jim Balter
0

Personne n'a encore souligné que ForEach <T> entraîne une vérification du type de temps de compilation où le mot-clé foreach est vérifié lors de l'exécution.

Après avoir effectué une refactorisation où les deux méthodes ont été utilisées dans le code, je suis favorable à .ForEach, car j'ai dû rechercher les échecs de test / échecs d'exécution pour trouver les problèmes foreach.

Écureuil
la source
5
Est-ce vraiment le cas? Je ne vois pas comment la foreachvérification du temps de compilation est moindre. Bien sûr, si votre collection est un IEnumerable non générique, la variable de boucle sera un object, mais la même chose serait vraie pour une ForEachméthode d'extension hypothétique .
Richard Poole
1
Ceci est mentionné dans la réponse acceptée. Cependant, c'est tout simplement faux (et Richard Poole ci-dessus a raison).
Jim Balter