1. Wprowadzenie
Wyobraź sobie, że wchodzisz do biblioteki i musisz znaleźć wszystkie książki o programowaniu wydane po 2020 roku, posortowane według nazwiska autora. Raczej nie będziesz biegać od półki do półki i sprawdzać każdą książkę ręcznie, prawda? Poprosiłbyś bibliotekarza, który wie, jak szybko znaleźć to, czego potrzebujesz.
LINQ (Language Integrated Query, czyli "Zapytania zintegrowane z językiem") – to nasz "sprytny bibliotekarz", "SQL-silnik" (czyli narzędzie do pisania zapytań do danych) prosto w C#!
Myśl o LINQ jak o języku, który pozwala Ci opisać, co chcesz dostać z danych, a nie jak to zrobić. Zamiast wymagać: "Weź pierwszy element, sprawdź warunek, jeśli pasuje, dodaj do nowej listy, potem przejdź do drugiego...", po prostu mówisz: "Daj mi wszystkie produkty, których cena jest większa niż 1000". A C# sam ogarnie, jak to zrobić najwydajniej.
Kluczowa idea LINQ: dać standardową składnię do zapytań do dowolnych źródeł danych, które implementują interfejs IEnumerable<T>. I to jest super, bo List<T>, tablice, HashSet<T> – wszystkie one implementują IEnumerable<T>. A jeśli Twoje dane są w bazie, to specjalne biblioteki (np. Entity Framework) zamienią Twoje LINQ-zapytania na prawdziwy SQL!
LINQ pojawił się w C# ponad 10 lat temu. To było przełomowe wydarzenie, które zmieniło podejście do pracy z danymi w .NET. Przed LINQ programiści musieli pisać mnóstwo szablonowego kodu do filtrowania, sortowania i transformacji kolekcji. Albo używać stringów z SQL-zapytaniami, które nie były sprawdzane przez kompilator i były podatne na błędy w czasie działania. LINQ przyniósł koncepcję "zapytań" prosto do języka programowania, czyniąc je typ-bezpiecznymi i bardziej czytelnymi.
Wielu uważa, że LINQ — to jedno z najważniejszych ulepszeń w C# od czasu pojawienia się generyków (Generics).
2. Zalety LINQ: Dlaczego wszyscy go kochają?
Zwięzłość i czytelność kodu: Żeby przefiltrować np. produkty z ceną powyżej 1000 w dużej bazie, trzeba było pisać kilka linijek kodu. Z LINQ można to wyrazić w jednej. Mniej kodu — mniej potencjalnych bugów, większa czytelność i zrozumiałość. Kod staje się bliższy naturalnemu językowi.
Programistyczny żart: "Im mniej kodu piszę, tym mniej bugów mogę tam wrzucić." LINQ w tym pomaga!
Moc i elastyczność: LINQ daje bogaty zestaw operacji: filtrowanie (Where), projekcja (Select), sortowanie (OrderBy), grupowanie (GroupBy), agregacja (Sum, Average), łączenie (Join) i wiele więcej. Pozwala rozwiązywać złożone zadania przetwarzania danych, łącząc te operacje.
Typ-bezpieczeństwo: To bardzo ważne! Gdy piszesz SQL-zapytanie jako string, kompilator nic o nim nie wie. Jeśli pomylisz się w nazwie kolumny, dowiesz się o tym dopiero w czasie działania programu, gdy się wywali z błędem. Z LINQ kompilator C# sprawdza Twoje zapytanie na błędy już podczas kompilacji. Jeśli spróbujesz zapytać o nieistniejące pole w obiekcie Product, kompilator od razu pokaże błąd. To jak mieć osobistego korektora, który łapie Twoje literówki zanim zobaczą je inni.
Integracja z językiem: Zapytania LINQ – to nie jakieś "magiczne" stringi. To pełnoprawne konstrukcje C#, które używają znanych Ci wyrażeń lambda i działają na już znanych typach danych. Dzięki temu przejście do LINQ jest bardzo płynne.
Odroczone wykonanie (Deferred Execution): To jedna z najfajniejszych i chyba najtrudniejszych koncepcji LINQ, ale bardzo ważna. Chodzi o to, że LINQ-zapytanie nie wykonuje się od razu, jak tylko je napiszesz. Ono się "składa" i czeka, aż naprawdę poprosisz o wynik (np. zaczniesz je iterować w foreach). To pozwala optymalizować zapytania, zwłaszcza przy pracy z dużą ilością danych. O tym pogadamy szerzej w Wykładzie 166. Na razie po prostu zapamiętaj: LINQ jest sprytny, nie robi niepotrzebnej roboty.
Uniwersalność i rozszerzalność: LINQ — to nie tylko dla list w pamięci. Istnieją różne "provider'y" LINQ:
- LINQ to Objects: dla kolekcji w pamięci (List<T>, tablice itd.).
- LINQ to SQL / Entity Framework Core: dla baz danych SQL (Twoje LINQ-zapytania zamieniają się w SQL-zapytania).
- LINQ to XML: do pracy z dokumentami XML.
- I wiele innych.
To znaczy, że jak ogarniesz LINQ, możesz pracować z danymi z różnych źródeł, używając tej samej logiki zapytań.
3. Problem pracy z kolekcjami "po staremu"
Przed LINQ kod obsługi kolekcji w C# wyglądał mniej więcej tak:
var products = new List<Product> { /* ... */ };
var expensive = new List<Product>();
foreach (var prod in products)
{
if (prod.Price > 100)
expensive.Add(prod);
}
expensive.Sort((a, b) => a.Price.CompareTo(b.Price));
foreach (var item in expensive)
Console.WriteLine(item.Name);
Taki sposób jest typowy: najpierw ręcznie przefiltrować, potem posortować — koniecznie stworzyć pośrednią listę. Im więcej logiki, tym więcej kodu, błędów i zmiennych. To wszystko przypomina warsztat składania mebli: części są, ale składanie — ręczne i męczące.
Alicja w krainie pętli
Wyobraź sobie, że masz ogromną listę użytkowników i chcesz dostać tylko imiona tych, którzy mają więcej niż 18 lat i mieszkają w mieście Neonville, posortować je alfabetycznie i wypisać trzy pierwsze. Musiałbyś pisać zagnieżdżone pętle, warunki, sortowania, pomocnicze listy... albo po prostu użyć LINQ.
4. Jak wygląda LINQ-zapytanie? Proste przykłady
LINQ daje dwa główne style składni:
- Method Syntax (łańcuch metod)
- Query Syntax (język podobny do SQL)
Najczęściej używa się Method Syntax, zwłaszcza w nowoczesnych projektach. Oto przykład na bazie naszej aplikacji:
var expensive = products
.Where(p => p.Price > 100)
.OrderBy(p => p.Price)
.Select(p => p.Name)
.Take(3);
foreach (var name in expensive)
Console.WriteLine(name);
Wszystko czytelne: filtr, sortowanie, wybór konkretnego pola, ograniczenie ilości. Minimum kodu — maksimum sensu.
To samo przez Query Syntax
var expensive = from p in products
where p.Price > 100
orderby p.Price
select p.Name;
foreach (var name in expensive.Take(3))
Console.WriteLine(name);
Oba warianty dają ten sam wynik — wybierz ten, który bardziej Ci pasuje (ale method-syntax i tak jest częściej używany).
5. Architektura LINQ: co pod maską?
LINQ działa z dowolnymi kolekcjami, które implementują interfejs IEnumerable<T> (albo IQueryable<T>, o nim pogadamy później).
Główne komponenty LINQ
| Komponent | Krótkie wyjaśnienie |
|---|---|
| Klasy rozszerzające LINQ | Statyczne metody (, , itd.) w klasie |
| Delegaty | Najczęściej używane i |
| Odroczone wykonanie | Zapytanie jest wyliczane dopiero przy pierwszej iteracji (np. przez ) |
| Query Provider | (dla LINQ to SQL, LINQ to Entities) — zamienia łańcuch metod na SQL-zapytania itd. |
Wizualny schemat
Kolekcja (List<Product>, T[], ...)
│
▼
LINQ-metody (Where, OrderBy, Select...)
│
▼
Zapytanie (IEnumerable<T>)
│
▼
Rzeczywiste wykonanie (foreach, ToList, Count, ...)
6. Przykład: krok po kroku analiza LINQ-łańcucha w naszej aplikacji
Wracamy do naszego mini-aplikacji z klasą Product:
// Klasa produktu
class Product
{
public string Name { get; set; }
public double Price { get; set; }
}
Oto zestaw produktów:
var products = new List<Product>
{
new Product { Name = "Ser", Price = 250.5 },
new Product { Name = "Chleb", Price = 30 },
new Product { Name = "Mleko", Price = 80 },
new Product { Name = "Kawa", Price = 330 },
new Product { Name = "Masło", Price = 140 }
};
Załóżmy, że naszym zadaniem jest: wypisać na ekran nazwy wszystkich towarów droższych niż 100 euro, posortować po cenie.
Stary sposób
var filtered = new List<Product>();
foreach (var p in products)
{
if (p.Price > 100)
filtered.Add(p);
}
filtered.Sort((a, b) => a.Price.CompareTo(b.Price));
foreach (var p in filtered)
Console.WriteLine(p.Name);
Sposób LINQ
var expensive = products
.Where(p => p.Price > 100)
.OrderBy(p => p.Price)
.Select(p => p.Name);
foreach (var name in expensive)
Console.WriteLine(name);
W pierwszej linijce opisujemy samą istotę zadania: "Przefiltruj tych, co mają Price > 100, posortuj po cenie, wybierz nazwy".
7. Częste operacje LINQ i ich analogi "po staremu"
| Operacja | Logika "po staremu" | LINQ |
|---|---|---|
| Filtrowanie | foreach + if + Add | |
| Projekcja (wybór pola) | foreach + Add(field) | |
| Sortowanie | Sort(comparer) | , |
| Unikalne wartości | foreach + contains + Add | |
| Zliczanie | foreach + counter++ | , |
| Sprawdzenie warunku | foreach + if | , |
| Szukanie pierwszego/ostatniego | foreach + if + break | , , |
| Ograniczenie ilości | foreach + licznik + break | , |
8. Praktyka: dodajemy LINQ do aplikacji
Weźmy bazowy kod naszej aplikacji (Product-lista) i spróbujmy zrobić kilka przydatnych operacji z LINQ.
Filtrowanie i sortowanie
// Wybierz produkty tańsze niż 200 euro i posortuj po nazwie
var cheapProducts = products
.Where(p => p.Price < 200)
.OrderBy(p => p.Name);
foreach (var p in cheapProducts)
Console.WriteLine($"{p.Name}: {p.Price} euro");
Transformacja (projekcja) kolekcji
// Pobierz listę cen (double)
var prices = products.Select(p => p.Price);
foreach (var price in prices)
Console.WriteLine(price);
Szukanie pierwszego pasującego elementu
// Pierwszy produkt, którego nazwa zaczyna się na "K"
var firstK = products.FirstOrDefault(p => p.Name.StartsWith("K"));
Console.WriteLine(firstK?.Name ?? "Nie znaleziono");
Zliczanie produktów droższych niż 100 euro
int count = products.Count(p => p.Price > 100);
Console.WriteLine($"Takich produktów: {count} szt.");
Od tego wykładu będziemy aktywnie używać LINQ do obsługi kolekcji, filtrowania, wyboru potrzebnych danych, agregacji i wielu innych rzeczy. Nowe metody i możliwości LINQ (w tym nowości z .NET 9!) omówimy w najbliższych wykładach, a na razie — nie bój się eksperymentować z prostymi zapytaniami, żeby poczuć całą moc tego narzędzia!
GO TO FULL VERSION