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 foreach
porte 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' foreach
abord:
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:
- On nous renvoie un objet avec état avec une connaissance approfondie de la collection sous-jacente.
- 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 Dispose
correctement 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 Dispose
problè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 foreach
styles 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é.