Pętla foreach w C# — jak działa pod maską

Pętla foreach służy do wygodnego przejścia po wszystkich elementach tablicy, łańcucha znaków lub kolekcji — bez ręcznego operowania indeksem.
Jak działa foreach
Pętla pobiera kolejno każdy element kolekcji i udostępnia go w zmiennej iteracyjnej. Jej największa zaleta to brak ryzyka przekroczenia indeksu tablicy lub bufora — nie musisz pilnować zakresu ani długości. Działa z każdą kolekcją implementującą IEnumerable<T> (tablice, listy, słowniki itd.) i świetnie sprawdza się z wynikami zapytań LINQ.
Składnia
foreach (typ identyfikator in kolekcja)
{
// instrukcje wykonywane dla każdego elementu
}- kolekcja – tablica lub kolekcja, po której iterujemy (wymagane).
- typ – typ pojedynczego elementu (możesz użyć
var, by kompilator wywnioskował go sam). - identyfikator – nazwa zmiennej reprezentującej bieżący element.
Przykłady
// iteracja po znakach łańcucha
string tekst = "Abc";
foreach (char znak in tekst)
{
Console.WriteLine(znak); // A, b, c
}
// iteracja po liście
List<string> imiona = new() { "Anna", "Marek", "Ola" };
foreach (string imie in imiona)
{
Console.WriteLine(imie);
}Co wolno, a czego nie wolno w foreach
To częste źródło nieporozumień, więc rozdzielmy dwie rzeczy:
- Nie wolno zmieniać struktury kolekcji w trakcie iteracji — dodawanie czy usuwanie elementów rzuci
InvalidOperationException(„Collection was modified”). - Nie wolno przypisać nowej wartości zmiennej iteracyjnej — jest ona tylko do odczytu.
- Wolno natomiast modyfikować właściwości elementów, jeśli są to typy referencyjne (obiekty) — zmieniasz wtedy stan obiektu, a nie samą kolekcję.
// ✅ modyfikacja właściwości obiektu (typ referencyjny) — to działa
foreach (var produkt in produkty)
{
produkt.Cena *= 1.23m; // zmieniamy stan istniejącego obiektu
}
// ❌ zmiana struktury kolekcji w trakcie iteracji — wyjątek w czasie działania
foreach (var produkt in produkty)
{
produkty.Remove(produkt); // InvalidOperationException
}Jeśli musisz usuwać elementy podczas przechodzenia, użyj pętli for idącej od końca, metody RemoveAll(...) albo iteruj po kopii kolekcji (np. foreach (var p in produkty.ToList())).
Potrzebujesz indeksu? Użyj LINQ
Gdy mimo wszystko potrzebujesz numeru elementu, możesz połączyć foreach z metodą Select:
foreach (var (imie, index) in imiona.Select((wartosc, i) => (wartosc, i)))
{
Console.WriteLine($"{index + 1}. {imie}");
}Jak foreach działa pod maską
Kompilator tłumaczy foreach na wywołania metody GetEnumerator() kolekcji oraz pętlę korzystającą z MoveNext() i Current zwróconego enumeratora. Powyższy przykład z listą imion kompiluje się w przybliżeniu do:
using var enumerator = imiona.GetEnumerator();
while (enumerator.MoveNext())
{
string imie = enumerator.Current;
Console.WriteLine(imie);
}To dlatego zmiana struktury kolekcji w trakcie iteracji rzuca wyjątek — większość wbudowanych kolekcji śledzi wersję (licznik modyfikacji) i MoveNext() sprawdza go przy każdym kroku, wykrywając niespójność.
Co ciekawe, List<T> zwraca enumerator jako strukturę (value type), a nie klasę — dzięki temu iterowanie po niej w foreach nie alokuje nic na stercie. Jeśli jednak przypiszesz tę samą kolekcję do zmiennej typu interfejsu IEnumerable<T>, kompilator musi wywoływać metody przez interfejs, co powoduje boxing struktury enumeratora i drobny narzut wydajnościowy. W typowym kodzie biznesowym ta różnica jest pomijalna, ale w gorących ścieżkach wykonania (hot paths) bywa zauważalna.
Kiedy używać foreach?
Wybieraj foreach, gdy chcesz po prostu przejść po wszystkich elementach i indeks nie jest potrzebny — kod jest wtedy czytelniejszy i odporny na błędy zakresu. Gdy potrzebujesz indeksu, iterujesz wstecz lub modyfikujesz kolekcję — sięgnij po for.
💡 W Visual Studio: wpisz
foreachi naciśnij dwukrotnie Tab, aby wstawić szkielet pętli.
Jeśli potrzebujesz indeksu iteracji albo pełnej kontroli nad krokiem pętli, zobacz Pętla for w C#. Przegląd wszystkich pętli w C# znajdziesz w Pętle w C#.
FAQ
Dlaczego dostaję InvalidOperationException tylko czasem, gdy modyfikuję kolekcję w foreach?
Bo kolekcja wykrywa modyfikację przy kolejnym wywołaniu MoveNext() — jeśli usuniesz ostatni element w ostatniej iteracji, pętla może się zakończyć zanim licznik wersji zostanie sprawdzony ponownie. To niebezpieczne poleganie na przypadku, nie gwarantowane zachowanie — zawsze traktuj modyfikację kolekcji w foreach jako błąd, niezależnie od tego, czy akurat rzuci wyjątek.
Czy foreach działa na własnych typach?
Tak, o ile typ implementuje IEnumerable/IEnumerable<T> (albo ma publiczną metodę GetEnumerator() — kompilator nie wymaga formalnie interfejsu, tzw. duck typing). Najprostszy sposób to użycie słowa kluczowego yield return we własnej metodzie zwracającej IEnumerable<T>.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →
3 comments