1. Wprowadzenie
Wiesz co jest ciekawe? Wyrażenie lambda samo w sobie — to jak przepis bez garnka. Opisuje, co zrobić, ale potrzebuje „pojemnika”, żeby żyć w kodzie. W C# do tego są gotowe uniwersalne typy delegatów: Func, Action i Predicate.
Wyobraź sobie je jako gotowe formy dla twoich lambd — bierzesz odpowiednią i wlewasz swoją logikę. Żadnych ręcznie składanych wynalazków i własnych typów delegatów, gdy pod ręką jest wszystko, czego potrzeba.
Trochę historii
Na początku, gdy pojawiała się potrzeba przekazać funkcję jako parametr, trzeba było deklarować własny typ delegata. To było długo, uciążliwe i wyglądało tak:
delegate int Calculate(int x, int y);
Calculate adder = (a, b) => a + b;
Kiedy w C# pojawiły się Func, Action i Predicate, można było zapomnieć o ręcznym deklarowaniu delegatów w ~90% przypadków. Wygląda teraz dużo prościej i bardziej uniwersalnie:
Func<int, int, int> adder = (a, b) => a + b;
2. Func<T, TResult> — funkcja z wartością zwracaną
Składnia i przeznaczenie
Func — to generyczny delegat (generic delegate), który przyjmuje od 0 do 16 parametrów i zwraca wartość.
Func<int, int, int> sum = (x, y) => x + y;
Func<typ1, typ2, ..., typN, TResult> — wszystkie parametry oprócz ostatniego to argumenty, ostatni to typ zwracany.
Przykłady
1. Suma dwóch liczb
Func<int, int, int> sum = (x, y) => x + y;
Console.WriteLine(sum(3, 5)); // 8
2. Podnoszenie do kwadratu
Func<int, int> square = x => x * x;
Console.WriteLine(square(4)); // 16
3. Brak parametrów
Func<string> greet = () => "Hello, lambda!";
Console.WriteLine(greet());
Wizualny schemat
| Sygnatura | Przykład | Opis |
|---|---|---|
|
|
Przyjmuje int, zwraca int |
|
|
Dwa int, zwraca int |
|
|
Nie przyjmuje nic, zwraca string |
Jak to wygląda w twojej aplikacji
Powiedzmy, że w naszym mini-aplikacji (warunkowy "Katalog użytkowników") mamy listę liczb — chcemy zastosować do niej różne przetworzenia. Na przykład podnieść do kwadratu albo policzyć sumę z ustaloną liczbą:
List<int> numbers = new() { 1, 2, 3, 4, 5 };
Func<int, int> square = x => x * x;
var squares = numbers.Select(square);
Console.WriteLine(string.Join(", ", squares)); // 1, 4, 9, 16, 25
3. Action<T> — akcja bez zwrotu
Action — uniwersalny delegat do metod, które coś robią (np. wypisują na ekran), ale nic nie zwracają.
Może przyjmować od 0 do 16 parametrów, ale zawsze zwraca void.
Przykłady
1. Wypisanie na ekran
Action<string> print = text => Console.WriteLine("Data: " + text);
print("Hello, world!");
2. Akcja bez parametrów
Action greet = () => Console.WriteLine("Welcome!");
greet();
3. Akcja z wieloma parametrami
Action<int, int> showSum = (a, b) => Console.WriteLine($"Sum: {a + b}");
showSum(2, 3); // Sum: 5
Wizualny schemat
| Sygnatura | Przykład | Opis |
|---|---|---|
|
|
Bez parametrów, bez zwrotu |
|
|
Jeden parametr |
|
|
Wiele parametrów |
W naszej aplikacji
Dodajmy do katalogu użytkowników metodę do wypisania wszystkich imion:
List<string> names = new() { "Anna", "Boris", "Vika" };
Action<string> printName = name => Console.WriteLine("User: " + name);
names.ForEach(printName);
// albo tak: names.ForEach(name => Console.WriteLine(name));
4. Predicate<T> — tak czy nie?
Gdy potrzebna jest funkcja, która zwróci tylko true albo false dla jednego parametru, użyj Predicate<T>. To po prostu delegat, który przyjmuje jeden parametr i zwraca bool.
Predicate — oficjalne "potrzebujemy sprawdzenia boolean" opakowanie dla lambdy.
Przykłady
1. Sprawdzić, czy liczba jest większa niż 5
Predicate<int> isGreaterThanFive = x => x > 5;
Console.WriteLine(isGreaterThanFive(3)); // false
Console.WriteLine(isGreaterThanFive(7)); // true
2. Użycie z metodą List<T>.Find
List<int> values = new() { 2, 4, 7, 10 };
int found = values.Find(isGreaterThanFive); // używa Predicate<int>
Console.WriteLine(found); // 7
3. Czy wszyscy dorośli?
List<int> ages = new() { 12, 19, 34 };
bool allAdults = ages.TrueForAll(age => age >= 18);
// TrueForAll przyjmuje Predicate<int>
Czym różni się od Func<T, bool>?
W praktyce są w dużej mierze wymienne. Nawet dokumentacja Microsoft mówi: "Predicate<T> — to po prostu Func<T, bool> dla specjalnych API". Ale niektóre metody standardowej biblioteki czasami wymagają właśnie Predicate.
5. Jak lambdy "wpisują się" w Func, Action, Predicate
Kiedy piszesz lambdę, C# analizuje: "Aha, jej forma pasuje do wymaganego delegata — można podstawić!"
Func<int, int> f1 = x => x * 2;
Action<string> a1 = text => Console.WriteLine(text);
Predicate<int> p1 = x => x < 10;
Wszędzie lambda! Ale pod maską — trzy różne delegaty z różnymi sygnaturami.
Zastosowanie na przykładzie "rzeczywistego" kodu
List<User> users = new() {
new User("Anna", 24),
new User("Boris", 17),
new User("Vika", 31),
};
// Funkcja zwracająca tylko dorosłych użytkowników (Predicate<User>)
List<User> adults = users.FindAll(user => user.Age >= 18);
Console.WriteLine("Spis dorosłych: " + string.Join(", ", adults.Select(u => u.Name)));
A jeśli chcemy wypisać imiona wszystkich użytkowników przez Action<User>:
users.ForEach(user => Console.WriteLine(user.Name));
Aby dostać ich imiona (Func<User, string>):
IEnumerable<string> names = users.Select(user => user.Name);
Tabela dla przejrzystości
| Delegat | Sygnatura | Przykład lambdy | Gdzie używany |
|---|---|---|---|
|
T → U | |
Select, wszelkie transformacje |
|
T → void | |
ForEach, metody-akcje |
|
T → bool | |
Find, Exists, filtry |
6. Przykłady z wewnętrzną aplikacją: krok po kroku
Rozszerzmy naszą małą aplikację "Katalog użytkowników". Niech będzie klasa User:
public class User
{
public string Name { get; }
public int Age { get; }
public bool IsActive { get; set; }
public User(string name, int age)
{
Name = name;
Age = age;
IsActive = true;
}
}
1. Func<User, bool> — sprawdzamy, czy użytkownik jest pełnoletni
Func<User, bool> isAdult = user => user.Age >= 18;
Używamy w LINQ:
var adults = users.Where(isAdult);
2. Predicate<User> — szukamy aktywnego użytkownika
Predicate<User> isActive = user => user.IsActive;
User found = users.Find(isActive);
3. Action<User> — dezaktywujemy użytkownika
Action<User> deactivate = user => user.IsActive = false;
users.ForEach(deactivate);
4. Func<User, string> — dostajemy krótkie opisanie
Func<User, string> describe = user => $"{user.Name} ({user.Age})";
var descriptions = users.Select(describe);
Wszystkie te lambdy — to całkiem realny "kod w postaci danych", który można przekazywać do metod, przechowywać w zmiennych, łączyć.
7. Nieoczywiste niuanse i typowe błędy
1. Lambda musi pasować do sygnatury delegata. Jeśli sygnatura nie zgadza się, będzie błąd kompilacji.
Func<int, string> wrong = x => x * 2; // błąd: oczekiwany string, otrzymano int
// Poprawnie:
Func<int, string> right = x => (x * 2).ToString();
2. Nie zapominaj o void vs return. Action nie zwraca wartości — próba napisania czegoś takiego: Action<int> a = x => x * x; nie zadziała, bo zwracana jest wartość, choć nie powinna być.
3. Predicate<T> i Func<T, bool> często są wymienne, ale nie zawsze. Czasem metody kolekcji oczekują dokładnie Predicate<T>, czasem — Func<T, bool>. Bezpośrednie przypisanie może nie działać bez jawnego opakowania.
Predicate<int> pred = x => x > 0;
Func<int, bool> func = pred; // błąd
// Ale:
Func<int, bool> func2 = x => x > 0;
Predicate<int> pred2 = new Predicate<int>(func2); // można przekonwertować przez konstruktor
GO TO FULL VERSION