1. Wprowadzenie
W prawdziwych projektach prawie co drugi programista spędza sporą część swojego życia na pracy z kolekcjami: filtruje, liczy, szuka, przekształca, wkłada, wyciąga — innymi słowy, traktuje je trochę jak lodówkę przed snem. Szczególnie często trzeba wyciągać, przetwarzać i agregować dane — czy to listy użytkowników, produkty w katalogu, wiersze tekstu czy dowolne inne tablice.
Prawie wszystkie nowoczesne kolekcje w .NET wspierają metody funkcyjne — takie jak Where, Select, Find, Any, All i inne. Ich siła tkwi w uniwersalności i zwięzłym stylu: po prostu przekazujesz "kawałek logiki" w postaci wyrażenia lambda i kolekcja ożywa, jakbyś uruchomił nowy silnik.
LINQ (Language Integrated Query) — to nie tylko syntaktyczny cukierek, ale cały mini-język w C#, pozwalający pisać zapytania do danych tak, jakbyś korzystał z SQL lub Excela. Tylko lepiej: prosto w kodzie, z autouzupełnianiem, typami i debugerem.
Ale cała ta magia działa dzięki delegatom — a za każdym razem pisać oddzielną metodę dla filtrowania to męczące. I tutaj wyrażenia lambda wchodzą do gry jako małe funkcje "na miejscu", zamieniając ciężki kod w elegancki i czytelny.
2. Wyrażenia lambda w standardowych metodach kolekcji
Wyrażenia lambda szczególnie dobrze sprawdzają się w standardowych metodach kolekcji opartych na delegatach, takich jak Find, Exists, ForEach i wielu innych.
Przykład: wyszukiwanie według warunku
Załóżmy, że masz listę produktów:
using System;
using System.Collections.Generic;
// Nasza klasa produktu
public class Product
{
public string Name { get; set; }
public int Price { get; set; }
}
var products = new List<Product>
{
new Product { Name = "Kofe", Price = 100 },
new Product { Name = "Czaj", Price = 70 },
new Product { Name = "Moloko", Price = 80 }
};
// Znajdź pierwszy drogi produkt (>90)
Product expensive = products.Find(p => p.Price > 90); // Używamy lambdy!
Console.WriteLine(expensive?.Name); // => Kofe
Bez lambdy musiałbyś napisać oddzielną metodę lub anonimową funkcję w starym stylu. Tak — jedna linia i kod czyta się jak angielski: "Znajdź produkt, gdzie cena większa niż 90".
Przykład: sprawdzenie obecności produktu
bool hasCheap = products.Exists(p => p.Price < 75);
Console.WriteLine(hasCheap); // => True (bo "Czaj" tańszy niż 75)
Przykład: przetworzenie wszystkich elementów (ForEach)
Czasem trzeba coś zrobić z każdym elementem:
products.ForEach(p => Console.WriteLine($"{p.Name}: {p.Price} euro"));
O analogiach
Krótko: wyrażenia lambda w kolekcjach to jak przycisk "upiększ" w edytorze zdjęć. Klikasz — i efekt gotowy!
3. Wyrażenia lambda i LINQ: magia dla kolekcji
LINQ — to nie tylko wygoda, to też świetne wprowadzenie do stylu funkcyjnego. Większość metod LINQ oczekuje delegatów — a więc ich idealnym partnerem są wyrażenia lambda.
Filtrowanie z Where
Niech znów będzie lista produktów. Teraz spróbujemy wybrać tylko "tanie" produkty:
using System.Linq;
var cheapProducts = products.Where(p => p.Price < 90);
foreach (var p in cheapProducts)
Console.WriteLine(p.Name); // Czaj, Moloko
Dostaliśmy nową kolekcję bez pisania żadnej pętli ręcznie. Where przyjmuje lambda-predicate (funkcję zwracającą true/false) i stosuje ją do każdego elementu.
Sortowanie z OrderBy
Dla osób lubiących porządek:
var sorted = products.OrderBy(p => p.Price);
foreach (var p in sorted)
Console.WriteLine($"{p.Name}: {p.Price}");
// Czaj: 70
// Moloko: 80
// Kofe: 100
Mapowanie (Select) – projekcja danych
Czasami potrzebujemy nie całego obiektu, a tylko jego części, np. listy nazw produktów:
var names = products.Select(p => p.Name);
foreach (var name in names)
Console.WriteLine(name); // Kofe, Czaj, Moloko
Łańcuchy LINQ
LINQ jest fajny, bo możesz "łączyć" wywołania jedno po drugim:
var namesOfCheap = products
.Where(p => p.Price < 90)
.OrderBy(p => p.Name)
.Select(p => p.Name.ToUpper());
foreach (var name in namesOfCheap)
Console.WriteLine(name); // MOLOKO, CZAJ
Wygląda jak linia produkcyjna: każdy metod to kolejny etap przetwarzania.
Pytanie: Dlaczego wyrażenia lambda są lepsze niż zwykłe metody dla LINQ?
Po pierwsze, lambdy możesz pisać dokładnie tam, gdzie są potrzebne. Po drugie, wyrażenia lambda są krótkie i czytelne. Po trzecie, to standard współczesnego C# — tak pisze większość, a ci którzy nie, często mają problem na rozmowach rekrutacyjnych.
4. Praktyczny przykład
W trakcie kursu pisaliśmy aplikację do pracy z małym katalogiem produktów, użytkowników lub zamówień. Dodajmy do niej nowoczesne metody przetwarzania kolekcji.
Wyszukiwanie użytkownika po nazwie
public class User
{
public string Username { get; set; }
public int Age { get; set; }
}
var users = new List<User>
{
new User{ Username = "Alice", Age = 21 },
new User{ Username = "Bob", Age = 26 },
new User{ Username = "Charlie", Age = 32 }
};
// Wyszukiwanie użytkownika po nazwie
User found = users.FirstOrDefault(u => u.Username == "Bob");
Console.WriteLine(found?.Age); // 26
Filtrowanie po wieku
var adults = users.Where(u => u.Age >= 18);
foreach (var u in adults)
Console.WriteLine(u.Username); // Alice, Bob, Charlie
Zliczanie użytkowników
int count = users.Count(u => u.Age > 25);
Console.WriteLine(count); // 2 (Bob i Charlie)
Sprawdzenie czy wszyscy użytkownicy są pełnoletni
bool allAdults = users.All(u => u.Age >= 18);
Console.WriteLine(allAdults); // True
Czy jest przynajmniej jeden niepełnoletni?
bool hasMinor = users.Any(u => u.Age < 18);
Console.WriteLine(hasMinor); // False
5. LINQ: jak to działa od środka
Kiedy piszesz np. Where(u => u.Age > 20), w rzeczywistości to mniej więcej to samo, co stworzenie pętli, która iteruje wszystkie elementy i sprawdza warunek dla każdego. Tylko LINQ robi to ładnie i niewidocznie, opakowując twój predykat w delegat.
Gdyby nie wyrażenia lambda, trzeba by używać takich konstrukcji:
public static bool AgeMoreThan20(User u) => u.Age > 20;
var adultUsers = users.Where(AgeMoreThan20);
Albo w ogóle anonimowe metody w "oldschoolowym" stylu:
var adultUsers = users.Where(delegate(User u) { return u.Age > 20; });
To wszystko jest ciężkie i nudne. Z lambdą — elegancko i nowocześnie.
6. Delegaty i standardowe typy: Func, Action, Predicate
Nie tylko LINQ uwielbia wyrażenia lambda. Wiele metod kolekcji przyjmuje wyspecjalizowane delegaty, np.:
- Predicate<T> — dla metod Find, Exists, RemoveAll
- Func<T, TResult> — dla metod LINQ, projekcji, obliczeń
- Action<T> — dla metod, które robią coś z elementem, ale nic nie zwracają (ForEach)
Tak to wygląda w praktyce:
// Predicate<T>
users.RemoveAll(u => u.Age < 30); // Usunęliśmy wszystkich młodszych niż 30
// Func<T, TResult>
var names = users.Select(u => u.Username);
// Action<T>
users.ForEach(u => Console.WriteLine(u.Username));
7. ściągawka po metodach kolekcji z wyrażeniami lambda
| Metoda | Co robi | Typ delegata | Przykład lambdy |
|---|---|---|---|
|
Filtruje elementy | |
|
|
Projekcja, transformacja | |
|
|
Sortowanie po kluczu | |
|
|
Pierwszy element pasujący do warunku | |
|
|
Czy istnieje choć jeden element pasujący do warunku | |
|
|
Czy wszystkie elementy spełniają warunek | |
|
|
Liczba elementów pasujących do warunku | |
|
|
Dla każdego elementu wykonaj coś | |
|
|
Usuwa wszystkie elementy według predykatu | |
|
8. Typowe błędy i cechy
Jednym z najczęstszych błędów jest zapomnieć, że LINQ nie zmienia oryginalnej kolekcji, tylko zwraca nową sekwencję. Czyli po kodzie var sorted = users.OrderBy(u => u.Age); kolekcja users zostanie w oryginalnym porządku! To może zmylić: czasem wydaje się, że wszystko jest posortowane — a tak naprawdę nie.
Kolejna rzecz: metody takie jak Where, Select i inne zwracają obiekty typu IEnumerable<T>. To "leniwa" kolekcja — prawdziwe przetwarzanie zaczyna się, gdy rzeczywiście zaczynasz ją enumerować (foreach, ToList() itp.). Jeśli chcesz zmaterializować wynik, pamiętaj, by wywołać ToList() lub ToArray():
var sortedList = users.OrderBy(u => u.Age).ToList();
Pamiętaj też: jeśli lambda odwołuje się do zmiennych spoza swojego zakresu (closures), to te zmienne "żyją" dopóki żyje odniesienie do lambdy. Nie jest to bardzo groźne, ale jeśli używasz lambdy wewnątrz długożyjącego obiektu i złapałeś w niej "ogromną tablicę", to ta tablica będzie trzymana w pamięci razem z lambdą.
I jeszcze: używaj opisowych nazw parametrów i zmiennych — to bardzo poprawia czytelność, zwłaszcza gdy masz kilka poziomów zagnieżdżonych lambd.
GO TO FULL VERSION