Menu

Pętla foreach w C#: tablice, List, Dictionary, indeks i błędy

Jak działa foreach w C#: przechodzenie po tablicach, listach, stringach i słownikach (KeyValuePair i dekonstrukcja), uzyskanie indeksu, dlaczego zmienna pętli jest tylko do odczytu, wyjątek InvalidOperationException "Collection was modified" i sposoby naprawy oraz czego potrzebuje typ, aby działał z foreach.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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) na List<T> usuwa wszystkie pasujące elementy w jednym przejściu.
  • Iteruj po kopii utworzonej przez ToList() albo ToArray(), 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 for od tyłu z RemoveAt(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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ