• C# i .NET
  • LINQ i foreach w C# bez pułapek - praktyczne zasady

LINQ i foreach w C# bez pułapek - praktyczne zasady

Przemysław Kwiatkowski 15 września 2026
Konwersja pętli `foreach` na zapytanie LINQ. Kod filtruje stringi krótsze niż 3 znaki.

Spis treści

Masz kolekcję obiektów, filtrujesz ją za pomocą LINQ i chcesz wykonać operację na każdym znalezionym elemencie? Najczęściej najlepszym rozwiązaniem będzie zwykła pętla foreach, ale sposób zapisania zapytania, moment jego wykonania i wybór między foreach, ForEach oraz ToList() mają duże znaczenie. Pokażę praktyczne przykłady, typowe błędy i sytuacje, w których pozornie poprawny kod może wykonać się więcej razy, niż zakładasz.

Najważniejsze zasady iterowania po wynikach LINQ

  • foreach wykonuje zapytanie LINQ, gdy zaczyna odczytywać jego elementy.
  • Where filtruje, a Select przekształca dane przed przejściem po kolekcji.
  • ForEach nie jest metodą LINQ, tylko metodą klasy List.
  • ToList() materializuje wynik i zatrzymuje go w pamięci jako listę.
  • Przy IQueryable trzeba uważać na wielokrotne wykonanie zapytania do bazy.

Konwersja pętli `foreach` na zapytanie LINQ. Kod filtruje krótkie pozdrowienia.

LINQ i foreach w C# w najprostszym wariancie

Najbardziej czytelny schemat wygląda tak: najpierw buduję zapytanie, a później przechodzę po jego wynikach za pomocą foreach. W tym przykładzie interesują mnie tylko aktywne produkty droższe niż 100 zł.

var products = new[]
{
    new { Name = "Klawiatura", Price = 180m, IsActive = true },
    new { Name = "Mysz", Price = 80m, IsActive = true },
    new { Name = "Monitor", Price = 900m, IsActive = false }
};

var expensiveActiveProducts = products
    .Where(product => product.IsActive && product.Price > 100)
    .OrderBy(product => product.Price);

foreach (var product in expensiveActiveProducts)
{
    Console.WriteLine($"{product.Name}: {product.Price} zł");
}

Where ogranicza zbiór, a OrderBy ustala kolejność. Samo przypisanie wyniku do zmiennej nie musi jeszcze uruchomić całej logiki. W przypadku typowych operatorów LINQ wynik jest obliczany dopiero wtedy, gdy zaczynam go enumerować, na przykład właśnie w pętli foreach.

To ważne, bo zmienna expensiveActiveProducts przechowuje przede wszystkim opis operacji, a niekoniecznie gotową listę elementów. Dlatego można powiedzieć, że pętla jest nie tylko sposobem odczytu danych, ale także momentem wykonania zapytania.

Co dokładnie robi foreach podczas iterowania

Pętla foreach korzysta z mechanizmu enumeracji, czyli odczytu kolejnych elementów sekwencji. Dla IEnumerable compiler wykorzystuje enumerator i wywołuje go krok po kroku, dzięki czemu nie musisz ręcznie obsługiwać indeksu ani sprawdzać, czy kolekcja ma jeszcze dane.

var numbers = Enumerable.Range(1, 10)
    .Where(number => number % 2 == 0)
    .Select(number => number * 10);

foreach (var number in numbers)
{
    Console.WriteLine(number);
}

Wynikiem będą liczby 20, 40, 60, 80 i 100. Najpierw działa filtr, później projekcja, czyli przekształcenie każdego elementu. Kolejność operatorów ma znaczenie, ponieważ wcześniejsze odfiltrowanie danych może ograniczyć liczbę elementów przetwarzanych przez następne operacje.

W praktyce często spotykam też próbę użycia Select do wykonywania efektów ubocznych, na przykład zapisu do logu. To zwykle zły kierunek, ponieważ Select służy do tworzenia nowej sekwencji. Gdy chcę wykonać akcję dla każdego elementu, wybieram foreach, bo kod od razu pokazuje swoją intencję.

foreach, ForEach i ToList mają różne zadania

Te trzy konstrukcje bywają wrzucane do jednego worka, choć rozwiązują różne problemy. Największe nieporozumienie polega na tym, że ForEach nie jest standardowym operatorem LINQ. Jest metodą dostępną między innymi dla List.

Rozwiązanie Kiedy działa Najważniejsza cecha
foreach Na dowolnym obiekcie możliwym do enumeracji Obsługuje break, continue i wyjątki w naturalny sposób
List.ForEach Tylko na liście Przyjmuje akcję w postaci delegata, ale gorzej skaluje się przy bardziej złożonej logice
ToList() Na sekwencji LINQ Materializuje wynik i zapisuje elementy w pamięci
var names = users
    .Where(user => user.IsActive)
    .Select(user => user.Name)
    .ToList();

foreach (var name in names)
{
    Console.WriteLine(name);
}

W tym wariancie ToList() kończy budowanie zapytania i tworzy konkretną listę. Przydaje się, gdy chcę wykonać zapytanie tylko raz, użyć wyników kilka razy albo mieć możliwość dodawania i usuwania elementów z kolekcji wynikowej.

Nie dodaję jednak ToList() automatycznie. Jeśli dane są duże, materializacja może niepotrzebnie zwiększyć zużycie pamięci. Gdy potrzebuję tylko jednorazowo przejść po wynikach, bezpośredni foreach jest prostszy i często korzystniejszy.

Wykonanie odroczone potrafi zmienić wynik

Wykonanie odroczone oznacza, że zapytanie LINQ jest obliczane dopiero przy enumeracji. Dzięki temu mogę zbudować zapytanie wcześniej, a dane zostaną odczytane dopiero wtedy, gdy naprawdę będą potrzebne. Ta wygoda ma jednak konsekwencję: wynik może zależeć od stanu źródła w momencie wykonania.

var numbers = new List { 1, 2, 3 };

var evenNumbers = numbers.Where(number => number % 2 == 0);

numbers.Add(4);

foreach (var number in evenNumbers)
{
    Console.WriteLine(number);
}

Wynik obejmie również liczbę 4, ponieważ lista została zmieniona przed wykonaniem pętli. Gdy zależy mi na zatrzymaniu konkretnego stanu danych, tworzę kopię przez ToList().

var evenNumbersSnapshot = numbers
    .Where(number => number % 2 == 0)
    .ToList();

numbers.Add(6);

foreach (var number in evenNumbersSnapshot)
{
    Console.WriteLine(number);
}

Drugi ważny problem to wielokrotna enumeracja. Każde przejście po zapytaniu może ponownie uruchomić filtr, sortowanie, a przy IQueryable także zapytanie do bazy. Jeśli wynik będzie używany kilka razy, świadomie zdecyduj, czy potrzebujesz materializacji, zamiast liczyć na przypadkowe zachowanie.

Praktyczne przykłady z projekcją i grupowaniem

Największa siła połączenia LINQ z foreach pojawia się wtedy, gdy pętla dostaje już dane przygotowane dokładnie do konkretnego zadania. Nie muszę w środku sprawdzać wszystkich warunków, pobierać właściwości i budować obiektów pomocniczych.

var reportRows = orders
    .Where(order => order.Status == OrderStatus.Paid)
    .Select(order => new
    {
        order.Id,
        CustomerName = order.Customer.Name,
        Total = order.Items.Sum(item => item.Price * item.Quantity)
    })
    .OrderByDescending(row => row.Total);

foreach (var row in reportRows)
{
    Console.WriteLine(
        $"Zamówienie {row.Id}, klient: {row.CustomerName}, suma: {row.Total} zł");
}

Ten styl ogranicza odpowiedzialność pętli do prezentacji lub wykonania działania. W kodzie produkcyjnym szczególnie cenię sobie takie rozdzielenie, bo warunki i obliczenia są w zapytaniu, a efekt końcowy w pętli.

Jeśli logika filtrowania robi się dłuższa, wydzielam ją do osobnej metody. W projektach, w których często korzystam z pomocniczych funkcji bez stanu obiektu, przydatne może być także użycie metod static w C#. Trzeba jednak pamiętać, że metoda przekazywana do LINQ przez Entity Framework musi być możliwa do przetłumaczenia na zapytanie źródłowe, jeśli pracujemy na IQueryable.

Przy grupowaniu pojawia się z kolei naturalna pętla zagnieżdżona.

var productsByCategory = products
    .Where(product => product.IsActive)
    .GroupBy(product => product.Category);

foreach (var group in productsByCategory)
{
    Console.WriteLine(group.Key);

    foreach (var product in group)
    {
        Console.WriteLine($"- {product.Name}");
    }
}

To czytelniejsze niż ręczne budowanie słownika, gdy potrzebuję tylko przejść po kategoriach i ich elementach. GroupBy porządkuje dane przed iteracją, a nie zastępuje samej pętli.

Typowe błędy przy iterowaniu wyników LINQ

Modyfikowanie źródłowej kolekcji w pętli

Usuwanie elementów z listy podczas działania foreach zwykle kończy się wyjątkiem albo pominięciem części danych. Bezpieczniej utworzyć kopię elementów do usunięcia, użyć RemoveAll albo przygotować nową listę za pomocą Where i ToList().

var remainingUsers = users
    .Where(user => !user.IsBlocked)
    .ToList();

Wykonywanie tego samego zapytania kilka razy

Ten kod może wielokrotnie przejść po źródle:

if (query.Any())
{
    Console.WriteLine(query.Count());
}

foreach (var item in query)
{
    Process(item);
}

Przy zwykłej tablicy będzie to przede wszystkim dodatkowa praca procesora. Przy źródle bazodanowym może oznaczać trzy osobne operacje. Jeżeli potrzebuję jednego zestawu danych, często materializuję go raz i dopiero później sprawdzam jego zawartość.

Mylenie IEnumerable z IQueryable

IEnumerable oznacza przetwarzanie elementów po stronie aplikacji. IQueryable pozwala dostawcy, na przykład ORM, przełożyć część operacji na zapytanie źródłowe. To rozróżnienie wpływa na wydajność, liczbę pobieranych kolumn i ilość danych przesyłanych z bazy.

Przeczytaj również: Async i await w C# bez blokowania wątku - praktyczny przewodnik

Nieprawidłowe porównywanie statusów

Jeżeli elementy mają status zapisany jako typ wyliczeniowy, filtr powinien porównywać wartości tego samego typu, a nie przypadkowe liczby lub teksty.

var paidOrders = orders
    .Where(order => order.Status == OrderStatus.Paid);

foreach (var order in paidOrders)
{
    Process(order);
}

Przy pracy ze statusami i flagami dobrze znać zasady bezpiecznego użycia enum w C#. Czytelna nazwa wartości zwykle wygrywa z magiczną liczbą, ale sam typ wyliczeniowy nie zabezpieczy aplikacji przed błędnym mapowaniem danych z zewnętrznego źródła.

Kiedy zwykły foreach nie wystarczy

Standardowa pętla jest właściwa dla sekwencji synchronicznych. Jeśli dane napływają asynchronicznie, na przykład z paginowanego API albo strumienia, można potrzebować await foreach i typu IAsyncEnumerable.

await foreach (var message in GetMessagesAsync())
{
    await HandleMessageAsync(message);
}

Nie zamieniam jednak zwykłego foreach na wersję asynchroniczną tylko dlatego, że kod zawiera async. Asynchroniczna enumeracja ma sens wtedy, gdy samo źródło dostarcza dane etapami. Dla gotowej listy w pamięci niczego istotnego nie poprawi.

Podobnie nie każdy problem wymaga LINQ. Gdy pętla ma skomplikowaną logikę, kilka warunków wyjścia i obsługę błędów, klasyczny foreach bywa po prostu łatwiejszy do testowania. Zwięzłość zapisu jest zaletą tylko wtedy, gdy nie ukrywa działania kodu.

Dobry domyślny wybór dla codziennego kodu C#

Najczęściej zaczynam od zapytania LINQ, które filtruje, sortuje lub projektuje dane, a kończę je zwykłym foreach. Dostaję wtedy czytelny podział odpowiedzialności: LINQ przygotowuje dane, a pętla wykonuje operację.

ToList() dodaję świadomie, gdy potrzebuję migawki danych, wielokrotnego użycia wyniku albo kolekcji, którą będę modyfikować. Z kolei List.ForEach zostawiam dla bardzo krótkich akcji, w których brak break i continue nie będzie problemem.

Jeśli zapamiętasz jedną rzecz, niech będzie nią moment wykonania zapytania. To właśnie on najczęściej decyduje o tym, czy kod będzie przewidywalny, wydajny i bezpieczny w aplikacji .NET.

FAQ - Najczęstsze pytania

Typowe zapytania LINQ są wykonywane odroczone, więc samo przypisanie wyniku do zmiennej nie musi jeszcze uruchamiać filtrowania ani sortowania. Zapytanie zostaje wykonane podczas enumeracji, na przykład w pętli foreach. Jeśli potrzebujesz zatrzymać konkretny stan danych, użyj ToList().

foreach działa na dowolnej sekwencji możliwej do enumeracji i obsługuje między innymi break oraz continue. List<T>.ForEach jest metodą listy i przyjmuje akcję jako delegat. ToList() materializuje wynik zapytania w pamięci, dzięki czemu można użyć go wielokrotnie lub modyfikować utworzoną listę.

Każde przejście po niezmaterializowanym zapytaniu może ponownie uruchomić filtr, sortowanie i projekcję. Przy IQueryable oznacza to także kolejne zapytanie do bazy. Kod używający kolejno Any(), Count() i foreach może wykonać trzy osobne operacje, dlatego przy wielokrotnym użyciu warto rozważyć ToList().

IEnumerable<T> służy do przetwarzania elementów po stronie aplikacji. IQueryable<T> pozwala dostawcy, na przykład ORM, przełożyć część operacji na zapytanie źródłowe, co wpływa na liczbę pobieranych kolumn i danych przesyłanych z bazy. Metody przekazywane do zapytania muszą być możliwe do przetłumaczenia przez jego dostawcę.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

linq
iqueryable
materializacja
enumeracja
Autor Przemysław Kwiatkowski
Przemysław Kwiatkowski
Jestem Przemysław i od 15 lat zajmuję się programowaniem .NET, chmurą Azure oraz sztuczną inteligencją. Moja przygoda z tymi technologiami zaczęła się od fascynacji możliwościami, jakie dają, a z czasem przerodziła się w pasję do tworzenia rozwiązań, które realnie wpływają na pracę i życie ludzi. Na kursdotnet.pl staram się dzielić się swoją wiedzą w sposób przystępny, tłumacząc złożone zagadnienia i pomagając zrozumieć, jak te dynamicznie rozwijające się obszary IT mogą być wykorzystane w praktyce. Dokładam wszelkich starań, aby prezentowane przeze mnie materiały były rzetelne, aktualne i oparte na sprawdzonych źródłach, a także aby uporządkować wiedzę w sposób ułatwiający jej przyswojenie.

Udostępnij artykuł

Napisz komentarz