🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Pętla foreach w C# — jak iterować po kolekcji

Pętla foreach działa tak samo niezależnie od typu kolekcji — ale to, co dokładnie zobaczysz podczas iteracji, różni się w zależności od tego, po jakiej strukturze danych iterujesz. Podstawy składni, mechanizm działania (GetEnumerator/MoveNext) i ograniczenia foreach omawiam szczegółowo w artykule Instrukcje iteracyjne — foreach. Tutaj skupiamy się na czymś innym: praktycznej galerii przykładów — jak wygląda foreach na każdej z głównych kolekcji C#, i jakie niespodzianki kryje kolejność iteracji w każdej z nich.

Tablica i List<T> — kolejność wstawiania

Najbardziej przewidywalny przypadek — elementy pojawiają się dokładnie w kolejności, w jakiej zostały dodane:

string[] monthsOfYear = { "Styczeń", "Luty", "Marzec" };

foreach (var month in monthsOfYear)
{
    Console.WriteLine(month);
}
// Styczeń, Luty, Marzec — zawsze w tej kolejności

List<string> cities = new List<string> { "Warszawa", "Kraków", "Gdańsk" };

foreach (string city in cities)
{
    Console.WriteLine(city);
}
// Warszawa, Kraków, Gdańsk — kolejność wstawiania

Dictionary<TKey, TValue> — KeyValuePair i dekonstrukcja

Iterując po Dictionary, każdy element to KeyValuePair<TKey, TValue>, nie sama wartość. Stary styl odczytuje .Key i .Value osobno:

var populacje = new Dictionary<string, int>
{
    ["Warszawa"] = 1_863_000,
    ["Kraków"] = 804_000,
};

// Styl klasyczny
foreach (KeyValuePair<string, int> wpis in populacje)
{
    Console.WriteLine($"{wpis.Key}: {wpis.Value}");
}

// Nowoczesna dekonstrukcja (C# 7+) — czytelniejsza
foreach (var (miasto, populacja) in populacje)
{
    Console.WriteLine($"{miasto}: {populacja}");
}

Dekonstrukcja działa, bo KeyValuePair<TKey, TValue> ma wbudowaną metodę Deconstruct — dokładnie ten sam mechanizm, który pozwala dekonstruować krotki i rekordy. Kolejność iteracji po Dictionary nie jest gwarantowana w kontrakcie API — w praktyce bywa zbliżona do kolejności wstawiania dla małych, niemodyfikowanych słowników, ale nie polegaj na tym.

HashSet<T> — brak gwarancji kolejności

var kody = new HashSet<string> { "PL", "DE", "FR" };

foreach (var kod in kody)
{
    Console.WriteLine(kod);
}
// Kolejność zależy od wewnętrznej tablicy haszującej — nie zakładaj PL, DE, FR

To najczęstsza pułapka przy przechodzeniu z List<T> na HashSet<T> — kod, który działał, bo “przypadkiem” kolejność się zgadzała, może się złamać po najmniejszej zmianie (dodaniu innego elementu, aktualizacji .NET). Jeśli kolejność ma znaczenie, potrzebujesz SortedSet<T> albo posortowania jawnie przez LINQ.

Queue<T> — od przodu do tyłu (FIFO)

var kolejka = new Queue<string>();
kolejka.Enqueue("Zgłoszenie #1");
kolejka.Enqueue("Zgłoszenie #2");
kolejka.Enqueue("Zgłoszenie #3");

foreach (var zgloszenie in kolejka)
{
    Console.WriteLine(zgloszenie);
}
// #1, #2, #3 — foreach NIE zdejmuje elementów, tylko je odczytuje w kolejności FIFO

Ważne: foreach po Queue<T> tylko odczytuje elementy w kolejności od przodu do tyłu — nie wywołuje Dequeue() i nie modyfikuje kolejki. Po pętli kolejka wciąż ma te same 3 elementy.

Stack<T> — od wierzchu do dołu, nie od dołu do wierzchu

var stos = new Stack<string>();
stos.Push("Pierwszy dodany");
stos.Push("Drugi dodany");
stos.Push("Trzeci dodany");

foreach (var element in stos)
{
    Console.WriteLine(element);
}
// "Trzeci dodany", "Drugi dodany", "Pierwszy dodany"
// — foreach zaczyna od WIERZCHU stosu, nie od pierwszego dodanego elementu!

To zaskakuje osoby przyzwyczajone do tego, że foreach zawsze idzie “od początku” — dla Stack<T> “początkiem” logicznym jest właśnie wierzch, zgodnie z zasadą LIFO. Jeśli chcesz przejść od najstarszego do najnowszego elementu, musisz jawnie odwrócić kolejność (np. stos.Reverse() z LINQ, co tworzy nową sekwencję i nie modyfikuje oryginalnego stosu).

Tablice wielowymiarowe

foreach po tablicy dwuwymiarowej spłaszcza ją do sekwencji wiersz-po-wierszu, bez informacji o pozycji — jeśli potrzebujesz indeksów wiersza/kolumny, użyj zagnieżdżonych pętli for:

int[,] macierz = { { 1, 2 }, { 3, 4 } };

foreach (int wartosc in macierz)
{
    Console.Write(wartosc + " ");
}
// 1 2 3 4 — kolejność wiersz po wierszu, bez współrzędnych

Ściąga — czego się spodziewać po kolejności

KolekcjaKolejność iteracjiGwarantowana?
Tablica / List<T>kolejność wstawiania / indeksuTak
Queue<T>od przodu do tyłu (FIFO)Tak
Stack<T>od wierzchu do dołu (LIFO)Tak
Dictionary / HashSet<T>zależna od tablicy haszującejNie
SortedSet<T>rosnąco wg komparatoraTak

Podsumowanie

foreach ma jeden, spójny mechanizm działania niezależnie od typu kolekcji — ale kolejność, w jakiej zobaczysz elementy, jest częścią kontraktu każdej konkretnej kolekcji, nie samego foreach. Najbezpieczniejsze założenie: jeśli kolejność ma znaczenie dla Twojej logiki, sprawdź w dokumentacji konkretnej kolekcji, czy w ogóle ją gwarantuje — dla Dictionary i HashSet odpowiedź brzmi “nie”, nawet jeśli w praktyce często wygląda przewidywalnie.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 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.

1 comment

Dodaj komentarz

czytanie to początek

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ę →