foreach führt seinen Rumpf einmal für jedes Element einer Collection aus, der Reihe nach, ohne Index, um den du dich kümmern musst:
Ausgabe:
Welcome, Ava
Welcome, Noah
Welcome, Mia
Total: 255
[C][#][7]
Die Schleife funktioniert genauso mit einem Array, einer List<T>, einem String (Elementtyp char), einem HashSet<T>, einer LINQ-Abfrage oder jeder anderen Sequenz. Der deklarierte Typ kann var sein, was üblich ist, wenn der Elementtyp lang ist: foreach (var order in pendingOrders).
Was foreach im Hintergrund tut
foreach fordert von der Collection einen Enumerator an und geht ihn durch. Bei einer List<int> macht der Compiler aus der Schleife ungefähr das hier:
List<int>.Enumerator e = scores.GetEnumerator();
try
{
while (e.MoveNext())
{
int score = e.Current;
// loop body
}
}
finally
{
e.Dispose();
}
Daraus folgen drei Dinge. Jeder Typ mit einer öffentlichen Methode GetEnumerator() funktioniert, die man normalerweise durch Implementieren von IEnumerable<T> bereitstellt. Der Enumerator wird freigegeben, auch wenn der Rumpf wirft oder break ausführt. Und die Elementvariable ist bei jedem Durchlauf eine frische Kopie von e.Current, deshalb kann eine Zuweisung an sie die Collection nicht ändern.
Ein Typ ohne GetEnumerator lässt sich nicht verwenden:
error CS1579: foreach statement cannot operate on variables of type 'Order' because 'Order' does not contain a public instance or extension definition for 'GetEnumerator'
Implementiere IEnumerable<T> für den Typ oder durchlaufe die Collection, die er enthält (order.Lines statt order). Eigene Sequenzen kannst du mit yield return schreiben, das auf der Seite zu IEnumerable und yield behandelt wird.
Ein Dictionary durchlaufen
Die Elemente eines Dictionarys sind KeyValuePair<TKey, TValue>-Werte mit den Properties Key und Value:
Ausgabe:
apples: 12
pears: 0
kiwis: 30
<apples><pears><kiwis>
Units in stock: 42
30 kiwis
12 apples
0 pears
Ein Dictionary garantiert keine Reihenfolge beim Durchlaufen. In der Praxis folgt es der Einfügereihenfolge, bis du Einträge entfernst, aber Code, der von der Reihenfolge abhängt, sollte explizit sortieren, wie es die letzte Schleife mit OrderByDescending tut, oder ein SortedDictionary verwenden.
Ab .NET Core 2.0 (und in jedem modernen .NET) hat KeyValuePair eine Methode Deconstruct, das Paar lässt sich also direkt im Schleifenkopf in zwei Variablen aufteilen:
// modern .NET: KeyValuePair deconstruction
foreach (var (fruit, count) in stock)
{
Console.WriteLine($"{fruit}: {count}");
}
Das ist die kürzeste Form und die, die du in aktuellem Code siehst. Unter .NET Framework kompiliert sie nicht, weil KeyValuePair dort kein Deconstruct hat; nimm dort item.Key und item.Value.
Den Index bekommen
foreach hat keinen eigenen Index. Die zwei üblichen Wege zu einem Index sind ein Zähler, den du selbst führst, oder die Überladung von Select aus LINQ, die den Index zusammen mit jedem Element weitergibt:
Ausgabe:
1. Kenji
2. Sofia
3. Omar
index 0: Kenji
index 1: Sofia
index 2: Omar
Der Zähler ist am günstigsten und am klarsten. Wenn du den Index verwendest, um podium[i - 1] zu lesen oder podium[i] = ... zu schreiben, wechsle zu einer for-Schleife, die genau dafür gebaut ist.
Die Schleifenvariable ist schreibgeschützt
Du kannst der Iterationsvariable nichts zuweisen:
foreach (string name in names)
{
name = name.Trim();
// error CS1656: Cannot assign to 'name' because it is a 'foreach iteration variable'
}
Selbst wenn es erlaubt wäre, würde es nur eine lokale Kopie ändern. Um jedes Element zu transformieren, baue eine neue Sequenz (names.Select(n => n.Trim()).ToArray()) oder nimm eine for-Schleife, die names[i] = names[i].Trim() schreibt.
Was du ändern kannst, hängt davon ab, ob das Element eine Klasse oder ein Struct ist. Bei einer Klasse enthält die Variable eine Referenz, eine geänderte Property ändert also das Objekt in der Collection:
Ausgabe:
Order 1: shipped
Order 2: shipped
Wäre Order ein struct, scheiterte dieselbe Zuweisung mit CS1654 (Cannot modify members of 'order' because it is a 'foreach iteration variable'), weil die Variable eine Kopie des Werts ist, keine Referenz darauf.
Die Collection ändern: InvalidOperationException
Elemente einer List oder eines Dictionary hinzuzufügen oder zu entfernen, während ein foreach sie durchläuft, macht den Enumerator kaputt. Das nächste MoveNext wirft InvalidOperationException mit der Meldung „Collection was modified; enumeration operation may not execute.“:
Ausgabe:
InvalidOperationException: collection was modified
milk, bread
milk, bread
Die Lösungen, von der einfachsten zur flexibelsten:
RemoveAll(predicate)auf einerList<T>entfernt alle Treffer in einem Durchgang.- Eine Kopie durchlaufen mit
ToList()oderToArray(), dann das Original beliebig ändern. - Erst sammeln, danach anwenden: Sammle während der Schleife die zu löschenden Schlüssel in einer separaten Liste und entferne sie danach. Das ist das übliche Muster für ein
Dictionary. - Eine rückwärts laufende
for-Schleife mitRemoveAt(i).
Beim Dictionary gibt es eine Ausnahme, die du kennen solltest. Seit .NET Core 3.0 wirft es nicht mehr, wenn du während eines foreach darüber den Wert eines bestehenden Schlüssels überschreibst (stock[key] = 0) oder Einträge entfernst, während das Hinzufügen eines neuen Schlüssels weiterhin wirft. Unter .NET Framework werfen alle drei, erst die Schlüssel zu sammeln ist also die Variante, die überall funktioniert.
break, continue und null-Collections
break und continue verhalten sich wie in jeder anderen Schleife:
Ausgabe:
reading 21
reading 23
no tags, no crash
Eine null-Collection zu durchlaufen wirft NullReferenceException, wenn GetEnumerator aufgerufen wird. ?? Enumerable.Empty<T>() macht aus einer möglicherweise null seienden Sequenz eine leere; noch besser: Gib aus deinen eigenen Methoden leere Collections statt null zurück.
foreach gegenüber List.ForEach
List<T> hat eine Methode ForEach(Action<T>): names.ForEach(n => Console.WriteLine(n));. Das ist nicht dasselbe wie die Schleife. Ihr Rumpf ist ein Lambda, break, continue und yield stehen also nicht zur Verfügung, und return beendet nur den Aufruf für das aktuelle Element. Sie existiert nur auf List<T>, nicht auf Arrays oder anderen Sequenzen. Bevorzuge die foreach-Anweisung; nimm ForEach nur für eine einzeilige Aktion, die du schon als Methode hast.
Häufig gestellte Fragen
Wie durchlaufe ich in C# ein Dictionary mit foreach?
Jedes Element ist ein KeyValuePair<TKey, TValue>: foreach (var pair in prices) Console.WriteLine($"{pair.Key}: {pair.Value}");. Ab .NET Core 2.0 kannst du es dekonstruieren: foreach (var (name, price) in prices). Um nur die Schlüssel oder Werte zu durchlaufen, nimm prices.Keys oder prices.Values.
Wie bekomme ich in einer C#-foreach-Schleife den Index?
foreach hat keinen eingebauten Index. Führe einen Zähler (int i = 0; vor der Schleife, i++ am Ende des Rumpfs) oder projiziere jedes Element mit LINQ zusammen mit seinem Index: foreach (var item in names.Select((name, index) => new { name, index })). Wenn du den Index zum Zurückschreiben oder zum Vergleichen von Nachbarn brauchst, passt eine for-Schleife besser.
Was bedeutet „Collection was modified; enumeration operation may not execute“?
Du hast in einer List, einem Dictionary oder einer anderen Collection Elemente hinzugefügt oder entfernt, während ein foreach sie durchlief. Der Enumerator erkennt die Änderung und wirft beim nächsten Schritt InvalidOperationException. Durchlaufe eine Kopie (foreach (var x in list.ToList())), sammle die Änderungen und wende sie nach der Schleife an, nimm list.RemoveAll(...) oder eine rückwärts laufende for-Schleife.
Warum kann ich der foreach-Variable in C# nichts zuweisen?
Die Iterationsvariable ist schreibgeschützt: name = name.Trim(); innerhalb von foreach (string name in names) ist Fehler CS1656. Eine Zuweisung würde die Collection ohnehin nicht ändern. Um Elemente zu ersetzen, nimm eine for-Schleife mit Index oder baue mit Select eine neue Collection. Eine Property eines Klassenobjekts zu ändern (order.Status = ...) ist erlaubt.
Kann man in C# aus einer foreach-Schleife mit break aussteigen?
Ja. break beendet die Schleife sofort, und continue springt zum nächsten Element, genau wie bei for und while. return verlässt die ganze Methode. Die Methode List<T>.ForEach ist anders: Ihr Rumpf ist ein Lambda, dort stehen break und continue also nicht zur Verfügung.