CodeGym /Kursy /C# SELF /Wyrażenia lambda w kolekcjach i

Wyrażenia lambda w kolekcjach i LINQ

C# SELF
Poziom 49 , Lekcja 2
Dostępny

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
Where
Filtruje elementy
Func<T, bool>
p => p.Price > 100
Select
Projekcja, transformacja
Func<T, U>
p => p.Name
OrderBy
Sortowanie po kluczu
Func<T, K>
u => u.Age
FirstOrDefault
Pierwszy element pasujący do warunku
Func<T, bool>
u => u.Username == "Bob"
Any
Czy istnieje choć jeden element pasujący do warunku
Func<T, bool>
u => u.Age < 18
All
Czy wszystkie elementy spełniają warunek
Func<T, bool>
u => u.Age >= 18
Count
Liczba elementów pasujących do warunku
Func<T, bool>
p => p.Price > 50
ForEach
Dla każdego elementu wykonaj coś
Action<T>
u => Console.WriteLine(u.Name)
RemoveAll
Usuwa wszystkie elementy według predykatu
Predicate<T>
u => u.Age < 18

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.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION