foreach wykonuje swoje ciało raz dla każdego elementu kolekcji, po kolei, bez indeksu do pilnowania:
Wynik:
Welcome, Ava
Welcome, Noah
Welcome, Mia
Total: 255
[C][#][7]
Pętla działa tak samo dla tablicy, List<T>, stringa (typ elementu char), HashSet<T>, zapytania LINQ i każdej innej sekwencji. Zadeklarowanym typem może być var, co jest częste przy długich nazwach typów elementów: foreach (var order in pendingOrders).
Co foreach robi pod spodem
foreach prosi kolekcję o enumerator i nim przechodzi. Dla List<int> kompilator zamienia pętlę mniej więcej na to:
List<int>.Enumerator e = scores.GetEnumerator();
try
{
while (e.MoveNext())
{
int score = e.Current;
// loop body
}
}
finally
{
e.Dispose();
}
Wynikają z tego trzy rzeczy. Działa każdy typ z publiczną metodą GetEnumerator(), którą zwykle zapewnia implementacja IEnumerable<T>. Enumerator jest zwalniany nawet wtedy, gdy ciało rzuci wyjątek lub wykona break. A zmienna elementu przy każdym przejściu jest nową kopią e.Current, dlatego przypisanie do niej nie może zmienić kolekcji.
Typu bez GetEnumerator nie da się użyć:
error CS1579: foreach statement cannot operate on variables of type 'Order' because 'Order' does not contain a public instance or extension definition for 'GetEnumerator'
Zaimplementuj IEnumerable<T> w tym typie albo iteruj po kolekcji, którą zawiera (order.Lines zamiast order). Własne sekwencje możesz pisać przez yield return, co omawia strona o IEnumerable i yield.
Przechodzenie po słowniku
Elementy słownika to wartości KeyValuePair<TKey, TValue> z właściwościami Key i Value:
Wynik:
apples: 12
pears: 0
kiwis: 30
<apples><pears><kiwis>
Units in stock: 42
30 kiwis
12 apples
0 pears
Dictionary nie obiecuje żadnej kolejności wyliczania. W praktyce trzyma się kolejności wstawiania, dopóki nie usuniesz wpisów, ale kod zależny od kolejności powinien sortować jawnie, jak ostatnia pętla z OrderByDescending, albo użyć SortedDictionary.
W .NET Core 2.0 i nowszych (oraz w każdym współczesnym .NET) KeyValuePair ma metodę Deconstruct, więc parę można rozbić na dwie zmienne bezpośrednio w nagłówku pętli:
// modern .NET: KeyValuePair deconstruction
foreach (var (fruit, count) in stock)
{
Console.WriteLine($"{fruit}: {count}");
}
To najkrótsza forma i właśnie ją zobaczysz w aktualnym kodzie. Nie kompiluje się w .NET Framework, gdzie KeyValuePair nie ma Deconstruct; tam używaj item.Key i item.Value.
Uzyskanie indeksu
foreach nie ma własnego indeksu. Dwa typowe sposoby, żeby go mieć, to licznik prowadzony samodzielnie albo przeciążenie Select z LINQ, które przekazuje indeks razem z każdym elementem:
Wynik:
1. Kenji
2. Sofia
3. Omar
index 0: Kenji
index 1: Sofia
index 2: Omar
Licznik jest najtańszy i najczytelniejszy. Jeśli zaczynasz używać indeksu, żeby czytać podium[i - 1] albo zapisywać podium[i] = ..., przejdź na pętlę for, która jest stworzona właśnie do tego.
Zmienna pętli jest tylko do odczytu
Nie możesz przypisać wartości do zmiennej iteracji:
foreach (string name in names)
{
name = name.Trim();
// error CS1656: Cannot assign to 'name' because it is a 'foreach iteration variable'
}
Nawet gdyby to było dozwolone, zmieniłoby tylko lokalną kopię. Aby przekształcić każdy element, zbuduj nową sekwencję (names.Select(n => n.Trim()).ToArray()) albo użyj pętli for, która zapisuje names[i] = names[i].Trim().
To, co możesz zrobić, zależy od tego, czy element jest klasą, czy strukturą. W przypadku klasy zmienna przechowuje referencję, więc zmiana właściwości zmienia obiekt w kolekcji:
Wynik:
Order 1: shipped
Order 2: shipped
Gdyby Order był typem struct, to samo przypisanie kończy się błędem CS1654 (Cannot modify members of 'order' because it is a 'foreach iteration variable'), bo zmienna jest kopią wartości, a nie referencją do niej.
Modyfikowanie kolekcji: InvalidOperationException
Dodawanie lub usuwanie elementów List albo Dictionary w czasie, gdy foreach po niej przechodzi, psuje enumerator. Następne MoveNext rzuca InvalidOperationException z komunikatem "Collection was modified; enumeration operation may not execute.":
Wynik:
InvalidOperationException: collection was modified
milk, bread
milk, bread
Sposoby naprawy, od najprostszego do najbardziej elastycznego:
RemoveAll(predicate)naList<T>usuwa wszystkie pasujące elementy w jednym przejściu.- Iteruj po kopii utworzonej przez
ToList()alboToArray(), a oryginał modyfikuj swobodnie. - Najpierw zbierz, potem zastosuj: w trakcie pętli gromadź klucze do usunięcia w osobnej liście, a potem je usuń. To typowy wzorzec dla
Dictionary. - Pętla
forod tyłu zRemoveAt(i).
Dictionary ma jeden wyjątek, który warto znać. Od .NET Core 3.0 nadpisanie wartości istniejącego klucza (stock[key] = 0) lub usuwanie wpisów w trakcie foreach po słowniku już nie rzuca wyjątku, ale dodanie nowego klucza nadal tak. W .NET Framework wszystkie trzy operacje rzucają wyjątek, więc wcześniejsze zebranie kluczy to wersja, która działa wszędzie.
break, continue i kolekcje null
break i continue działają jak w każdej innej pętli:
Wynik:
reading 21
reading 23
no tags, no crash
Przejście pętlą po kolekcji null rzuca NullReferenceException przy wywołaniu GetEnumerator. ?? Enumerable.Empty<T>() zamienia sekwencję, która może być null, w pustą; jeszcze lepiej, gdy twoje własne metody zwracają puste kolekcje zamiast null.
foreach a List.ForEach
List<T> ma metodę ForEach(Action<T>): names.ForEach(n => Console.WriteLine(n));. To nie to samo co pętla. Jej ciało jest lambdą, więc break, continue i yield są niedostępne, a return kończy tylko wywołanie dla bieżącego elementu. Istnieje wyłącznie w List<T>, nie w tablicach ani innych sekwencjach. Wybieraj instrukcję foreach; ForEach stosuj tylko do jednolinijkowej akcji, którą masz już jako metodę.
Najczęściej zadawane pytania
Jak przejść pętlą foreach po Dictionary w C#?
Każdy element to KeyValuePair<TKey, TValue>: foreach (var pair in prices) Console.WriteLine($"{pair.Key}: {pair.Value}");. W .NET Core 2.0 i nowszych możesz go zdekonstruować: foreach (var (name, price) in prices). Aby przejść tylko po kluczach albo wartościach, użyj prices.Keys lub prices.Values.
Jak uzyskać indeks w pętli foreach w C#?
foreach nie ma wbudowanego indeksu. Prowadź licznik (int i = 0; przed pętlą, i++ na końcu ciała) albo zmapuj każdy element razem z indeksem przez LINQ: foreach (var item in names.Select((name, index) => new { name, index })). Jeśli indeks jest potrzebny do zapisu albo porównania sąsiadów, lepiej pasuje pętla for.
Co oznacza "Collection was modified; enumeration operation may not execute"?
Elementy zostały dodane lub usunięte z List, Dictionary albo innej kolekcji w czasie, gdy foreach ją przechodziła. Enumerator wykrywa zmianę i w następnym kroku rzuca InvalidOperationException. Iteruj po kopii (foreach (var x in list.ToList())), zbierz zmiany i zastosuj je po pętli, użyj list.RemoveAll(...) albo pętli for od tyłu.
Dlaczego nie mogę przypisać wartości do zmiennej foreach w C#?
Zmienna iteracji jest tylko do odczytu: name = name.Trim(); wewnątrz foreach (string name in names) to błąd CS1656. Przypisanie i tak nie zmieniłoby kolekcji. Aby zastąpić elementy, użyj pętli for z indeksem albo zbuduj nową kolekcję przez Select. Zmiana właściwości obiektu klasy (order.Status = ...) jest dozwolona.
Czy można przerwać pętlę foreach w C#?
Tak. break natychmiast kończy pętlę, a continue przechodzi do następnego elementu, dokładnie jak w for i while. return opuszcza całą metodę. Metoda List<T>.ForEach działa inaczej: jej ciało to lambda, więc break i continue nie są tam dostępne.