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ść wstawianiaDictionary<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, FRTo 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 FIFOWaż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
| Kolekcja | Kolejność iteracji | Gwarantowana? |
|---|---|---|
Tablica / List<T> | kolejność wstawiania / indeksu | Tak |
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ącej | Nie |
SortedSet<T> | rosnąco wg komparatora | Tak |
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.
🚀 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ń.
1 comment
Dodaj komentarz
Musisz się zalogować, aby móc dodać komentarz.
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ę →
Krytykuj przyjaciela w cztery oczy, a chwal zawsze przy świadkach i podwładnych. Leonardo da Vinci & Tm.