foreach vs someList.ForEach () {}

167

Il existe apparemment de nombreuses façons de parcourir une collection. Curieux de savoir s'il y a des différences ou pourquoi vous utiliseriez un moyen plutôt que l'autre.

Premier type:

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

L'autre manière:

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

Je suppose que de prime abord, qu'au lieu du délégué anonyme que j'utilise ci-dessus, vous auriez un délégué réutilisable que vous pourriez spécifier ...

Bryce Fischer
la source
1
Je suggère de lire le blog d'Eric Lipperts "foreach" vs "ForEach"
Erik Philips

Réponses:

134

Il existe une distinction importante et utile entre les deux.

Parce que .ForEach utilise une forboucle pour itérer la collection, ceci est valide (éditer: avant .net 4.5 - l'implémentation a changé et les deux lancent):

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

alors que foreachutilise un énumérateur, donc ce n'est pas valide:

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

tl; dr: NE copiez PAS ce code dans votre application!

Ces exemples ne sont pas des meilleures pratiques, ils servent simplement à démontrer les différences entre ForEach()et foreach.

La suppression d'éléments d'une liste dans une forboucle peut avoir des effets secondaires. Le plus courant est décrit dans les commentaires sur cette question.

En règle générale, si vous souhaitez supprimer plusieurs éléments d'une liste, vous souhaiterez séparer la détermination des éléments à supprimer de la suppression réelle. Il ne garde pas votre code compact, mais il garantit que vous ne manquerez aucun élément.


la source
35
même dans ce cas, vous devriez utiliser someList.RemoveAll (x => x.RemoveMe) à la place
Mark Cidade
2
Avec Linq, tout peut être amélioré. Je
2
RemoveAll () est une méthode sur List <T>.
Mark Cidade
3
Vous en êtes probablement conscient, mais les gens devraient se méfier de la suppression d'éléments de cette façon; si vous supprimez l'élément N, l'itération sautera l'élément (N + 1) et vous ne le verrez pas dans votre délégué ni ne pourrez le supprimer, comme si vous l'aviez fait dans votre propre boucle for.
El Zorko
9
Si vous parcourez la liste en arrière avec une boucle for, vous pouvez supprimer des éléments sans aucun problème de décalage d'index.
S. Tarık Çetin
78

Nous avons eu du code ici (dans VS2005 et C # 2.0) où les ingénieurs précédents ont fait tout leur possible pour l'utiliser list.ForEach( delegate(item) { foo;});au lieu de foreach(item in list) {foo; };tout le code qu'ils ont écrit. par exemple, un bloc de code pour lire les lignes d'un dataReader.

Je ne sais toujours pas exactement pourquoi ils ont fait ça.

Les inconvénients de list.ForEach()sont:

  • Il est plus détaillé en C # 2.0. Cependant, à partir de C # 3, vous pouvez utiliser la =>syntaxe " " pour créer des expressions joliment concises.

  • C'est moins familier. Les personnes qui doivent maintenir ce code se demanderont pourquoi vous l'avez fait de cette façon. Il m'a fallu un certain temps pour décider qu'il n'y avait aucune raison, sauf peut-être pour rendre l'écrivain intelligent (la qualité du reste du code minait cela). Il était également moins lisible, avec le " })" à la fin du bloc de code délégué.

  • Voir aussi le livre de Bill Wagner "Effective C #: 50 Specific Ways to Improve Your C #" où il explique pourquoi foreach est préféré à d'autres boucles comme les boucles for ou while - le point principal est que vous laissez le compilateur décider de la meilleure façon de construire la boucle. Si une future version du compilateur parvient à utiliser une technique plus rapide, vous l'obtiendrez gratuitement en utilisant foreach et la reconstruction, plutôt que de modifier votre code.

  • une foreach(item in list)construction vous permet d'utiliser breakou continuesi vous devez quitter l'itération ou la boucle. Mais vous ne pouvez pas modifier la liste dans une boucle foreach.

Je suis surpris de voir que list.ForEachc'est légèrement plus rapide. Mais ce n'est probablement pas une raison valable de l'utiliser partout, ce serait une optimisation prématurée. Si votre application utilise une base de données ou un service Web qui, et non un contrôle de boucle, sera presque toujours là où le temps passe. Et l'avez-vous également comparé à une forboucle? Le list.ForEachpourrait être plus rapide en raison de l' utilisation qui en interne et une forboucle sans l'emballage serait encore plus rapide.

Je ne suis pas d'accord pour dire que la list.ForEach(delegate)version est "plus fonctionnelle" d'une manière significative. Il transfère une fonction à une fonction, mais il n'y a pas de grande différence dans le résultat ou l'organisation du programme.

Je ne pense pas que foreach(item in list)"dit exactement comment vous voulez que cela soit fait" - une for(int 1 = 0; i < count; i++)boucle fait cela, une foreachboucle laisse le choix du contrôle au compilateur.

Mon sentiment est, sur un nouveau projet, d'utiliser foreach(item in list)pour la plupart des boucles afin de respecter l'usage courant et pour la lisibilité, et de list.Foreach()ne l' utiliser que pour des blocs courts, lorsque vous pouvez faire quelque chose de plus élégamment ou de manière plus compacte avec l' =>opérateur C # 3 " ". Dans de tels cas, il se peut qu'il existe déjà une méthode d'extension LINQ plus spécifique que ForEach(). Voyez si Where(), Select(), Any(), All(), Max()ou l' une des nombreuses autres méthodes de LINQ ne l'ont pas déjà ce que vous voulez de la boucle.

Anthony
la source
Juste pour la curiosité ... regardez l'implémentation de Microsoft ... referencesource.microsoft.com/#mscorlib/system/collections/…
Alexandre
17

Pour le plaisir, j'ai sauté List dans le réflecteur et voici le C # résultant:

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

De même, le MoveNext dans Enumerator qui est utilisé par foreach est le suivant:

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

Le List.ForEach est beaucoup plus réduit que MoveNext - beaucoup moins de traitement - sera plus probablement JIT en quelque chose d'efficace.

De plus, foreach () allouera un nouvel Enumérateur quoi qu'il arrive. Le GC est votre ami, mais si vous faites la même chose foreach à plusieurs reprises, cela fera plus d'objets jetables, par opposition à la réutilisation du même délégué - MAIS - c'est vraiment un cas marginal. Dans une utilisation typique, vous verrez peu ou pas de différence.

socle
la source
1
Vous n'avez aucune garantie que le code généré par foreach sera le même entre les versions du compilateur. Le code généré pourra être amélioré par une future version.
Anthony
1
À partir de .NET Core 3.1, ForEach est toujours plus rapide.
jackmott le
13

Je connais deux choses obscures qui les rendent différents. Allez moi!

Tout d'abord, il y a le bogue classique de créer un délégué pour chaque élément de la liste. Si vous utilisez le mot-clé foreach, tous vos délégués peuvent finir par faire référence au dernier élément de la liste:

    // 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();

La méthode List.ForEach n'a pas ce problème. L'élément actuel de l'itération est passé par valeur en tant qu'argument au lambda externe, puis le lambda interne capture correctement cet argument dans sa propre fermeture. Problème résolu.

(Malheureusement, je pense que ForEach est un membre de List, plutôt qu'une méthode d'extension, bien qu'il soit facile de le définir vous-même afin que vous ayez cette fonctionnalité sur n'importe quel type énumérable.)

Deuxièmement, l'approche de la méthode ForEach a une limitation. Si vous implémentez IEnumerable à l'aide de yield return, vous ne pouvez pas faire de yield return dans le lambda. Donc, parcourir les éléments d'une collection afin de produire des éléments de retour n'est pas possible avec cette méthode. Vous devrez utiliser le mot clé foreach et contourner le problème de fermeture en faisant manuellement une copie de la valeur de la boucle actuelle à l'intérieur de la boucle.

Plus ici

Daniel Earwicker
la source
5
Le problème que vous mentionnez foreachest "corrigé" dans C # 5. stackoverflow.com/questions/8898925/…
StriplingWarrior
13

Je suppose que l' someList.ForEach()appel pourrait être facilement parallélisé alors que la normale foreachn'est pas si facile à exécuter en parallèle. Vous pouvez facilement exécuter plusieurs délégués différents sur différents cœurs, ce qui n'est pas si facile à faire avec un fichier normal foreach.
Juste mes 2 cents

Joachim Kerschbaumer
la source
2
Je pense qu'il voulait dire que le moteur d'exécution pouvait le paralléliser automatiquement. Sinon, foreach et .ForEach peuvent être parallélisés à la main à l'aide d'un thread du pool dans chaque délégué d'action
Isak Savo
@Isak mais ce serait une hypothèse incorrecte. Si la méthode anonyme hisse un local ou un membre, le runtime ne pourra pas facilement) pouvoir paralelliser
Rune FS
6

Comme on dit, le diable est dans les détails ...

La plus grande différence entre les deux méthodes d'énumération de collection est que foreachporte l'état, alors que ce ForEach(x => { })n'est pas le cas.

Mais allons un peu plus loin, car il y a certaines choses dont vous devez être conscient et qui peuvent influencer votre décision, et il y a certaines mises en garde dont vous devez être conscient lors du codage pour l'un ou l'autre cas.

Utilisons List<T>dans notre petite expérience pour observer le comportement. Pour cette expérience, j'utilise .NET 4.7.2:

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

Répétons cela avec d' foreachabord:

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

Nous pourrions développer cela en:

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

}

Avec l'enquêteur en main, en regardant sous les couvertures, nous obtenons:

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

Deux choses deviennent immédiatement évidentes:

  1. On nous renvoie un objet avec état avec une connaissance approfondie de la collection sous-jacente.
  2. La copie de la collection est une copie superficielle.

Ceci n'est bien sûr en aucun cas thread-safe. Comme indiqué ci-dessus, changer la collection pendant l'itération est juste un mauvais mojo.

Mais qu'en est-il du problème de l'invalidité de la collection lors de l'itération par des moyens extérieurs à nous qui se moquent de la collection pendant l'itération? Les meilleures pratiques suggèrent de versionner la collection pendant les opérations et l'itération, et de vérifier les versions pour détecter quand la collection sous-jacente change.

C'est là que les choses deviennent vraiment troubles. Selon la documentation Microsoft:

Si des modifications sont apportées à la collection, telles que l'ajout, la modification ou la suppression d'éléments, le comportement de l'énumérateur n'est pas défini.

Eh bien, qu'est-ce que cela signifie? À titre d'exemple, ce n'est pas parce que List<T>implémente la gestion des exceptions que toutes les collections implémentées IList<T>feront de même. Cela semble être une violation flagrante du principe de substitution de Liskov:

Les objets d'une superclasse doivent être remplaçables par des objets de ses sous-classes sans interrompre l'application.

Un autre problème est que l'énumérateur doit implémenter IDisposable- cela signifie une autre source de fuites de mémoire potentielles, non seulement si l'appelant se trompe, mais si l'auteur n'implémente pas Disposecorrectement le modèle.

Enfin, nous avons un problème à vie ... que se passe-t-il si l'itérateur est valide, mais que la collection sous-jacente a disparu? Nous avons maintenant un aperçu de ce qui était ... lorsque vous séparez la durée de vie d'une collection et de ses itérateurs, vous demandez des ennuis.

Examinons maintenant ForEach(x => { }):

names.ForEach(name =>
{

});

Cela s'étend à:

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

Il est important de noter ce qui suit:

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

Ce code n'alloue aucun énumérateur (rien à Dispose) et ne s'arrête pas pendant l'itération.

Notez que cela effectue également une copie superficielle de la collection sous-jacente, mais la collection est maintenant un instantané dans le temps. Si l'auteur n'implémente pas correctement une vérification pour la collection changeante ou devenant «périmée», l'instantané est toujours valide.

Cela ne vous protège en aucun cas du problème des problèmes de durée de vie ... si la collection sous-jacente disparaît, vous avez maintenant une copie superficielle qui pointe vers ce qui était ... mais au moins vous n'avez pas de Disposeproblème à traiter les itérateurs orphelins ...

Oui, j'ai dit itérateurs ... parfois il est avantageux d'avoir un état. Supposons que vous vouliez conserver quelque chose qui ressemble à un curseur de base de données ... peut-être que plusieurs foreachstyles Iterator<T>sont la voie à suivre. Personnellement, je n'aime pas ce style de conception car il y a trop de problèmes à vie et vous comptez sur les bonnes grâces des auteurs des collections sur lesquelles vous vous appuyez (à moins que vous n'écriviez littéralement tout vous-même à partir de zéro).

Il y a toujours une troisième option ...

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

Ce n'est pas sexy, mais il a des dents (excuses à Tom Cruise et au film The Firm )

C'est votre choix, mais maintenant vous le savez et cela peut être un choix éclairé.

Stacy Dudovitz
la source
5

Vous pouvez nommer le délégué anonyme :-)

Et vous pouvez écrire le second comme:

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

Ce que je préfère, et économise beaucoup de frappe.

Comme le dit Joachim, le parallélisme est plus facile à appliquer à la seconde forme.

Craig.Nicol
la source
2

Dans les coulisses, le délégué anonyme est transformé en une méthode réelle afin que vous puissiez avoir une surcharge avec le deuxième choix si le compilateur n'a pas choisi d'intégrer la fonction. En outre, toutes les variables locales référencées par le corps de l'exemple de délégué anonyme changeraient de nature en raison des astuces du compilateur pour masquer le fait qu'elle soit compilée vers une nouvelle méthode. Plus d'informations ici sur la façon dont C # fait cette magie:

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

jezell
la source
2

La fonction ForEach est membre de la classe générique List.

J'ai créé l'extension suivante pour reproduire le code interne:

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

Donc, à la fin, nous utilisons un foreach normal (ou une boucle pour si vous le souhaitez).

D'un autre côté, l'utilisation d'une fonction déléguée est juste une autre façon de définir une fonction, ce code:

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

est équivalent à:

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

ou en utilisant des expressions labda:

(s) => <process the string>
Pablo Caballero
la source
2

La portée entière de ForEach (fonction déléguée) est traitée comme une seule ligne de code (appelant la fonction) et vous ne pouvez pas définir de points d'arrêt ou entrer dans le code. Si une exception non gérée se produit, tout le bloc est marqué.

Peter Shen
la source
2

List.ForEach () est considéré comme plus fonctionnel.

List.ForEach()dit ce que vous voulez faire. foreach(item in list)dit aussi exactement comment vous voulez que cela soit fait. Cela laisse List.ForEachlibre de changer la mise en œuvre de la partie comment à l'avenir. Par exemple, une future version hypothétique de .Net pourrait toujours fonctionner List.ForEachen parallèle, en supposant qu'à ce stade, tout le monde a un certain nombre de cœurs de processeur qui sont généralement inactifs.

D'autre part, foreach (item in list)vous donne un peu plus de contrôle sur la boucle. Par exemple, vous savez que les éléments seront itérés dans une sorte d'ordre séquentiel, et vous pourriez facilement casser au milieu si un élément remplit certaines conditions.


Quelques remarques plus récentes sur cette question sont disponibles ici:

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

Joël Coehoorn
la source
1

La deuxième façon que vous avez montrée utilise une méthode d'extension pour exécuter la méthode déléguée pour chacun des éléments de la liste.

De cette façon, vous avez un autre appel de délégué (= méthode).

De plus, il est possible d'itérer la liste avec une boucle for .

EFrank
la source
1

Une chose dont il faut se méfier est de savoir comment quitter la méthode générique .ForEach - voir cette discussion . Bien que le lien semble dire que cette voie est la plus rapide. Je ne sais pas pourquoi - vous penseriez qu'ils seraient équivalents une fois compilés ...

Chris Kimpton
la source
0

Il y a un moyen, je l'ai fait dans mon application:

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

Vous pouvez utiliser le comme article de foreach

Dannick Bédard
la source