Menu

Boucle foreach en C# : tableaux, List, Dictionary, index et erreurs

Comment fonctionne foreach en C# : parcourir des tableaux, des listes, des chaînes et des dictionnaires (KeyValuePair et déconstruction), obtenir l'index, pourquoi la variable de boucle est en lecture seule, l'InvalidOperationException « Collection was modified » et ses corrections, et ce qu'un type doit avoir pour être utilisé dans foreach.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

foreach exécute son corps une fois pour chaque élément d'une collection, dans l'ordre, sans index à gérer :

Sortie :

Welcome, Ava
Welcome, Noah
Welcome, Mia
Total: 255
[C][#][7]

La boucle fonctionne de la même façon sur un tableau, une List<T>, une chaîne (type d'élément char), un HashSet<T>, une requête LINQ ou toute autre séquence. Le type déclaré peut être var, ce qui est courant quand le type d'élément est long : foreach (var order in pendingOrders).

Ce que fait foreach en coulisses

foreach demande un énumérateur à la collection et le parcourt. Pour une List<int>, le compilateur transforme la boucle à peu près en ceci :

List<int>.Enumerator e = scores.GetEnumerator();
try
{
    while (e.MoveNext())
    {
        int score = e.Current;
        // loop body
    }
}
finally
{
    e.Dispose();
}

Trois conséquences en découlent. Tout type doté d'une méthode publique GetEnumerator() fonctionne, ce qu'on obtient normalement en implémentant IEnumerable<T>. L'énumérateur est libéré même quand le corps lève une exception ou fait un break. Et la variable d'élément est une nouvelle copie de e.Current à chaque passage, c'est pourquoi l'affecter ne peut pas modifier la collection.

Un type sans GetEnumerator ne peut pas être utilisé :

error CS1579: foreach statement cannot operate on variables of type 'Order' because 'Order' does not contain a public instance or extension definition for 'GetEnumerator'

Implémentez IEnumerable<T> sur le type, ou parcourez la collection qu'il contient (order.Lines au lieu de order). Vous pouvez écrire vos propres séquences avec yield return, présenté sur la page IEnumerable et yield.

Parcourir un Dictionary

Les éléments d'un dictionnaire sont des valeurs KeyValuePair<TKey, TValue> avec les propriétés Key et Value :

Sortie :

apples: 12
pears: 0
kiwis: 30
<apples><pears><kiwis>
Units in stock: 42
 30 kiwis
 12 apples
  0 pears

Un Dictionary ne garantit aucun ordre d'énumération. En pratique, il suit l'ordre d'insertion tant que vous ne supprimez pas d'entrées, mais le code qui dépend de l'ordre doit trier explicitement, comme le fait la dernière boucle avec OrderByDescending, ou utiliser SortedDictionary.

Sur .NET Core 2.0 et plus (et tout .NET moderne), KeyValuePair a une méthode Deconstruct, donc la paire peut être séparée en deux variables directement dans l'en-tête de la boucle :

// modern .NET: KeyValuePair deconstruction
foreach (var (fruit, count) in stock)
{
    Console.WriteLine($"{fruit}: {count}");
}

C'est la forme la plus courte et celle que vous verrez dans le code actuel. Elle ne compile pas sur .NET Framework, où KeyValuePair n'a pas de Deconstruct ; utilisez-y item.Key et item.Value.

Obtenir l'index

foreach n'a pas d'index propre. Les deux façons habituelles d'en obtenir un sont un compteur que vous tenez vous-même, ou la surcharge Select de LINQ qui transmet l'index avec chaque élément :

Sortie :

1. Kenji
2. Sofia
3. Omar
index 0: Kenji
index 1: Sofia
index 2: Omar

Le compteur est la solution la moins coûteuse et la plus claire. Si vous vous retrouvez à utiliser l'index pour lire podium[i - 1] ou écrire podium[i] = ..., passez à une boucle for, qui est faite exactement pour cela.

La variable de boucle est en lecture seule

Vous ne pouvez pas affecter la variable d'itération :

foreach (string name in names)
{
    name = name.Trim();
    // error CS1656: Cannot assign to 'name' because it is a 'foreach iteration variable'
}

Même si c'était permis, cela ne modifierait qu'une copie locale. Pour transformer chaque élément, construisez une nouvelle séquence (names.Select(n => n.Trim()).ToArray()) ou utilisez une boucle for qui écrit names[i] = names[i].Trim().

Ce que vous pouvez faire dépend de la nature de l'élément, classe ou struct. Pour une classe, la variable contient une référence, donc modifier une propriété modifie l'objet dans la collection :

Sortie :

Order 1: shipped
Order 2: shipped

Si Order était une struct, la même affectation échouerait avec CS1654 (Cannot modify members of 'order' because it is a 'foreach iteration variable'), car la variable est une copie de la valeur, pas une référence vers elle.

Modifier la collection : InvalidOperationException

Ajouter ou supprimer des éléments d'une List ou d'un Dictionary pendant qu'un foreach le parcourt casse l'énumérateur. Le MoveNext suivant lève InvalidOperationException avec le message « Collection was modified; enumeration operation may not execute. » :

Sortie :

InvalidOperationException: collection was modified
milk, bread
milk, bread

Les corrections, de la plus simple à la plus souple :

  • RemoveAll(predicate) sur une List<T> supprime toutes les correspondances en un seul passage.
  • Parcourir une copie avec ToList() ou ToArray(), puis modifier librement l'original.
  • Rassembler d'abord, appliquer ensuite : pendant la boucle, collectez les clés à supprimer dans une liste séparée, puis supprimez-les après. C'est le schéma habituel pour un Dictionary.
  • Une boucle for à l'envers avec RemoveAt(i).

Dictionary a une exception utile à connaître. Depuis .NET Core 3.0, écraser la valeur d'une clé existante (stock[key] = 0) ou supprimer des entrées pendant un foreach sur ce dictionnaire ne lève plus d'exception, alors qu'ajouter une nouvelle clé en lève toujours une. Sur .NET Framework, les trois lèvent une exception ; collecter les clés d'abord est donc la version qui fonctionne partout.

break, continue et collections null

break et continue se comportent comme dans toutes les autres boucles :

Sortie :

reading 21
reading 23
no tags, no crash

Parcourir une collection null lève NullReferenceException à l'appel de GetEnumerator. ?? Enumerable.Empty<T>() transforme une séquence peut-être nulle en séquence vide ; mieux encore, renvoyez des collections vides plutôt que null depuis vos propres méthodes.

foreach ou List.ForEach

List<T> a une méthode ForEach(Action<T>) : names.ForEach(n => Console.WriteLine(n));. Ce n'est pas la même chose que la boucle. Son corps est une lambda, donc break, continue et yield ne sont pas disponibles, et return ne termine que l'appel pour l'élément courant. Elle n'existe que sur List<T>, pas sur les tableaux ni les autres séquences. Préférez l'instruction foreach ; réservez ForEach à une action d'une ligne que vous avez déjà sous forme de méthode.

Questions fréquentes

Comment parcourir un Dictionary avec foreach en C# ?

Chaque élément est un KeyValuePair<TKey, TValue> : foreach (var pair in prices) Console.WriteLine($"{pair.Key}: {pair.Value}");. Sur .NET Core 2.0 et plus, vous pouvez le déconstruire : foreach (var (name, price) in prices). Pour ne parcourir que les clés ou les valeurs, utilisez prices.Keys ou prices.Values.

Comment obtenir l'index dans une boucle foreach C# ?

foreach n'a pas d'index intégré. Tenez un compteur (int i = 0; avant la boucle, i++ à la fin du corps), ou projetez chaque élément avec son index grâce à LINQ : foreach (var item in names.Select((name, index) => new { name, index })). Si vous avez besoin de l'index pour écrire dans la collection ou comparer des voisins, une boucle for convient mieux.

Que signifie « Collection was modified; enumeration operation may not execute » ?

Vous avez ajouté ou supprimé des éléments d'une List, d'un Dictionary ou d'une autre collection pendant qu'un foreach l'énumérait. L'énumérateur détecte la modification et lève InvalidOperationException à l'étape suivante. Parcourez une copie (foreach (var x in list.ToList())), rassemblez les modifications et appliquez-les après la boucle, utilisez list.RemoveAll(...), ou une boucle for à l'envers.

Pourquoi ne peut-on pas affecter la variable du foreach en C# ?

La variable d'itération est en lecture seule : name = name.Trim(); dans foreach (string name in names) donne l'erreur CS1656. L'affecter ne modifierait de toute façon pas la collection. Pour remplacer des éléments, utilisez une boucle for avec un index, ou construisez une nouvelle collection avec Select. Modifier une propriété d'un objet de classe (order.Status = ...) est autorisé.

Peut-on sortir d'une boucle foreach en C# ?

Oui. break termine immédiatement la boucle et continue passe à l'élément suivant, exactement comme dans for et while. return quitte toute la méthode. La méthode List<T>.ForEach est différente : son corps est une lambda, donc break et continue n'y sont pas disponibles.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER