Gdy trzeba przejść po elementach listy, tablicy albo wynikach zapytania LINQ, pętla foreach zwykle jest najczytelniejszym wyborem. Pokazuję, jak działa w C#, z jakimi kolekcjami współpracuje, kiedy lepiej użyć for, jak bezpiecznie przerywać iterację oraz na co uważać przy modyfikowaniu danych i kodzie asynchronicznym.
Najważniejsze informacje o pętli foreach w C#
- Składnia wykonuje blok kodu dla każdego elementu kolekcji.
- Brak indeksu ogranicza ryzyko błędów typu off-by-one.
- break kończy pętlę, a continue pomija bieżącą iterację.
- foreach służy głównie do odczytu, a for sprawdza się wtedy, gdy potrzebujesz indeksu.
- await foreach obsługuje asynchroniczne strumienie danych.
Jak działa pętla foreach w C#
Najprostsza postać wygląda tak:
string[] languages = { "C#", "F#", "Visual Basic" };
foreach (string language in languages)
{
Console.WriteLine(language);
}
Za każdym obrotem pętli zmienna language otrzymuje kolejny element tablicy. Kod wykona się więc trzy razy, w kolejności przechowywania danych. Nie muszę deklarować indeksu, sprawdzać granicy tablicy ani ręcznie pobierać elementu pod numerem i.
W praktyce kompilator korzysta z mechanizmu enumeratora. Kolekcja udostępnia metodę GetEnumerator, a enumerator przechodzi przez elementy za pomocą MoveNext() i udostępnia aktualną wartość przez Current. Dlatego pętla działa nie tylko z tablicami i listami, ale również z typami implementującymi IEnumerable, IEnumerable albo obsługującymi odpowiedni wzorzec enumeracji.
Jawny typ i var
Typ elementu można podać wprost albo pozwolić kompilatorowi wywnioskować go za pomocą var:
List scores = new() { 72, 88, 95 };
foreach (int score in scores)
{
Console.WriteLine(score);
}
foreach (var score in scores)
{
Console.WriteLine(score);
}
Sam najczęściej wybieram jawny typ, gdy poprawia czytelność kodu, a var zostawiam tam, gdzie typ wynika jasno z deklaracji kolekcji. Przy skomplikowanych typach generycznych var potrafi wyraźnie odchudzić kod, ale nie powinien ukrywać ważnej informacji przed osobą, która będzie go utrzymywać.
Po jakich kolekcjach można iterować
foreach obsługuje najczęściej używane kolekcje .NET. Zadziała między innymi z tablicą, List, HashSet, słownikiem, wynikiem LINQ oraz własnym typem, który udostępnia poprawny enumerator.
| Kolekcja | Przykład elementu | Typowe zastosowanie |
|---|---|---|
| Tablica | int |
Przejście po stałym zestawie wartości |
List |
Product |
Odczyt lub przetwarzanie listy obiektów |
Dictionary |
KeyValuePair |
Praca z kluczami i wartościami |
IEnumerable |
T |
Iterowanie po wynikach LINQ lub danych generowanych leniwie |
W przypadku słownika mogę odczytać zarówno parę klucz-wartość, jak i przejść tylko po kluczach albo wartościach:
var prices = new Dictionary
{
["Laptop"] = 4200m,
["Monitor"] = 1300m
};
foreach (var price in prices)
{
Console.WriteLine($"{price.Key}: {price.Value} zł");
}
foreach (var productName in prices.Keys)
{
Console.WriteLine(productName);
}
Przy dużych strukturach danych nie zakładam automatycznie, że każda iteracja jest kosztowna. Sama pętla zwykle jest prosta i szybka, a większe znaczenie ma to, co wykonuję w jej środku. Jeśli jednak potrzebuję znać liczbę elementów, odróżniam Count w C# od Capacity, ponieważ pojęcia te opisują różne rzeczy.
Co robić podczas iteracji
Wewnątrz pętli można filtrować dane, wywoływać metody, budować wynik albo reagować na konkretną wartość. Najważniejsze jest to, aby zakres odpowiedzialności pętli był jasny. Gdy w środku pojawia się wiele niezależnych operacji, zwykle lepiej wydzielić część logiki do osobnej metody.
Pomijanie i kończenie pętli
var numbers = new[] { 3, 8, 11, 20, 25 };
foreach (int number in numbers)
{
if (number % 2 != 0)
{
continue;
}
if (number > 18)
{
break;
}
Console.WriteLine(number);
}
continue pomija resztę bieżącego obrotu i przechodzi do następnego elementu. break natychmiast kończy całą pętlę. Taki zapis jest zwykle czytelniejszy niż głęboko zagnieżdżone instrukcje if, szczególnie gdy warunek odrzucenia elementu jest prosty.
Modyfikowanie elementów
Zmienna iteracyjna jest domyślnie traktowana jako tylko do odczytu. Nie mogę więc przypisać jej nowej wartości:
foreach (int number in numbers)
{
// number = number * 2; // błąd kompilacji
}
To nie oznacza jednak, że nie da się zmienić właściwości obiektu referencyjnego:
foreach (var user in users)
{
user.IsActive = true;
}
W tym przypadku nie podmieniam zmiennej user, tylko modyfikuję obiekt, na który wskazuje. Jeśli chcę zmienić wartości typów prostych w liście, najczęściej tworzę nową kolekcję za pomocą LINQ albo używam pętli for z indeksem.
Nie zmieniaj rozmiaru kolekcji w trakcie foreach
Usuwanie lub dodawanie elementów do większości kolekcji podczas ich enumerowania kończy się wyjątkiem, często InvalidOperationException. Bezpieczny wariant polega na przygotowaniu osobnej listy elementów do usunięcia:
var expiredOrders = orders
.Where(order => order.IsExpired)
.ToList();
foreach (var order in expiredOrders)
{
orders.Remove(order);
}
Przy mniejszej liście można też iterować po kopii, na przykład orders.ToList(). Trzeba tylko pamiętać, że kopia zużywa dodatkową pamięć. Dla bardzo dużych kolekcji lepiej zaprojektować operację tak, aby nie tworzyć niepotrzebnych duplikatów danych.
Foreach czy for
Obie pętle potrafią przejść po tej samej tablicy, ale rozwiązują trochę inne problemy. Ja zaczynam od foreach, gdy interesuje mnie sam element. Po for sięgam wtedy, gdy indeks jest częścią logiki albo gdy muszę zmieniać elementy kolekcji po pozycji.
| Potrzeba | Lepszy wybór | Dlaczego |
|---|---|---|
| Odczyt wszystkich elementów | foreach |
Brak ręcznego indeksowania i prostszy kod |
| Praca z numerem pozycji | for |
Indeks jest dostępny w każdej iteracji |
| Zatrzymanie po spełnieniu warunku | Obie pętle | Obie obsługują break
|
| Zmiana wartości elementów listy | for |
Można przypisać wartość pod konkretnym indeksem |
for (int i = 0; i < numbers.Length; i++)
{
numbers[i] *= 2;
}
Nie zamieniam automatycznie każdej pętli na for z myślą o wydajności. W typowym kodzie aplikacyjnym większą różnicę robi liczba operacji wykonywanych dla elementu, zapytania do bazy danych i alokacje pamięci niż sam wybór między tymi konstrukcjami.
Warianty dla nowoczesnego C#
Asynchroniczne await foreach
Jeśli dane pojawiają się jako strumień asynchroniczny implementującyIAsyncEnumerable, używam await foreach. Każde pobranie kolejnego elementu może wtedy zaczekać bez blokowania wątku:
await foreach (var message in ReadMessagesAsync())
{
Console.WriteLine(message);
}
Zwykłe foreach i await foreach nie są zamienne. Pierwsza konstrukcja oczekuje synchronicznego enumeratora, a druga asynchronicznego GetAsyncEnumerator. Próba użycia niepasującej wersji kończy się błędem kompilacji.
De konstrukcja elementów
Gdy kolekcja przechowuje krotki, mogę od razu rozpakować ich elementy:
var products = new[]
{
("Klawiatura", 299m),
("Mysz", 149m)
};
foreach (var (name, price) in products)
{
Console.WriteLine($"{name}: {price} zł");
}
Ten zapis dobrze sprawdza się przy prostych danych pomocniczych. Przy większych modelach domenowych preferuję nazwany typ, ponieważ właściwości obiektu lepiej opisują znaczenie danych niż pozycje w krotce.
Przeczytaj również: public static w C# - jak działa i kiedy tego używać?
ref i ref readonly
Specjalistyczne warianty ref oraz ref readonly pozwalają pracować z elementami przez referencję, na przykład w przypadku Span. To narzędzie do kodu nastawionego na ograniczenie kopii i alokacji, a nie zamiennik zwykłej pętli w każdej aplikacji.
Span buffer = stackalloc int[3];
foreach (ref int value in buffer)
{
value = 10;
}
Sięgam po ten wariant dopiero wtedy, gdy profilowanie potwierdza problem z wydajnością albo pracuję nad kodem niskopoziomowym. Zwykły foreach pozostaje lepszym wyborem pod względem czytelności w większości projektów.
Typowe błędy, które łatwo przeoczyć
Pusta kolekcja nie powoduje błędu, ale ciało pętli nie wykona się ani razu. Z kolei przekazanie wartości null do zwykłego foreach kończy się NullReferenceException, dlatego przy danych opcjonalnych trzeba wcześniej ustalić, czy brak kolekcji jest dozwolony.
if (items is null)
{
return;
}
foreach (var item in items)
{
Process(item);
}
Drugim częstym problemem jest niejawna konwersja typu elementu. Jeśli jawnie zadeklarowany typ nie pasuje do rzeczywistej zawartości kolekcji, program może zgłosić InvalidCastException podczas działania. W takich sytuacjach var albo poprawne typowanie kolekcji często ujawnia problem wcześniej i upraszcza kod.
Trzeba też uważać na wielokrotne wykonywanie kosztownego zapytania LINQ. Jeżeli źródło jest leniwe, każda osobna iteracja może ponownie uruchomić obliczenia lub odczyt danych. Gdy potrzebuję stabilnego, jednorazowego wyniku, materializuję go przez ToList(), ale robię to świadomie, bo oznacza to dodatkową pamięć.
Jeżeli elementy kolekcji opisują stany, statusy lub role, czytelność poprawia nazwany typ wyliczeniowy. W takim przypadku przydaje się opisane osobno bezpieczne użycie enum w C#, zwłaszcza gdy wartości pochodzą z bazy danych albo z zewnętrznego API.
Jak wybrać dobrą pętlę w codziennym kodzie
Moja praktyczna zasada jest prosta. Używam foreach do czytania kolekcji, for do pracy z indeksem, a await foreach do asynchronicznego strumienia. Nie optymalizuję składni na zapas, tylko sprawdzam rzeczywiste koszty operacji wykonywanych wewnątrz pętli.
- Wybierz
foreach, gdy kolejność elementów i ich odczyt są najważniejsze. - Wybierz
for, gdy potrzebujesz indeksu albo zmieniasz wartości pod konkretną pozycją. - Użyj
break, gdy znalezienie wyniku kończy pracę. - Użyj
continue, gdy część elementów ma zostać pominięta. - Nie zmieniaj rozmiaru kolekcji podczas enumerowania.
- Przy danych asynchronicznych stosuj
await foreach, a nie blokujące oczekiwanie na wynik.
Dobrze użyta pętla foreach nie jest tylko krótszą wersją for. Przekazuje intencję kodu: „przetwórz każdy element tego zbioru”. Właśnie dlatego w aplikacjach .NET jest zwykle moim pierwszym wyborem, a po bardziej złożone konstrukcje sięgam dopiero wtedy, gdy konkretny przypadek naprawdę tego wymaga.
