1. Wprowadzenie
Wyobraź sobie, że masz ekspres do kawy. Zwykle po prostu parzy kawę i nie przeszkadza nikomu, ale czasem szef mówi: „A możesz, proszę, po zaparzeniu kawy zakrzyknąć ‘Gotowe!’?” — i tu potrzebna jest konfiguracja. Samego ekspresu nie przepisujemy. Po prostu tworzymy funkcję, którą on wywoła na końcu.
W C# tę rolę pełnią delegaty — pozwalają przekazywać do metod kawałki kodu (metody, lambdy albo metody anonimowe), żeby ta metoda wywołała je w odpowiednim momencie. Mówiąc prościej, delegat to typ, który może przechowywać referencje do metod o określonej sygnaturze.
Definicja delegata
W C# delegat definiuje się za pomocą słowa kluczowego delegate. Przykład:
// Delegat, który odnosi się do metod przyjmujących int i zwracających bool
public delegate bool PredicateInt(int x);
Teraz zmienna typu PredicateInt będzie mogła odnosić się do dowolnej metody (albo lambdy!), która przyjmuje jeden int i zwraca bool.
Do czego służą delegaty?
- Przekazywanie logiki jako argumentu (np. do sortowania, filtrowania, obsługi zdarzeń)
- Subskrypcja zdarzeń (o tym porozmawiamy później)
- Implementacja callbacków
- Elastyczne API, gdzie część zachowania definiuje strona wywołująca
Prosty schemat wizualny
| Typ delegata | Sygnatura | Przykład wywołania |
|---|---|---|
|
|
|
|
|
|
|
|
|
2. Jak lambda zamienia się w delegata
Składnia
Kiedy piszesz wyrażenie lambda, na przykład x => x > 5, w istocie tworzysz obiekt delegata. Lambda nie „żyje” w próżni: potrzebuje typu (ktoś musi wiedzieć, jaki ma zestaw parametrów i wartość zwracaną). Dlatego wyrażenie lambda w C# zawsze jest niejawnie (albo jawnie) rzutowane na delegata.
Przykład 1: Przypisanie lambdy do delegata
// Jawnie definiujemy delegata
public delegate bool MyPredicate(int number);
class Program
{
static void Main()
{
// Przypisujemy lambdę do zmiennej typu MyPredicate
MyPredicate isEven = x => x % 2 == 0;
Console.WriteLine(isEven(4)); // true
Console.WriteLine(isEven(7)); // false
}
}
Przykład 2: Użycie standardowych delegatów
C# zawiera zestaw standardowych generycznych delegatów: Action, Func<>, Predicate<>. Używa się ich niemal wszędzie, gdzie piszesz lambdy w kodzie.
// Używamy Func
Func
isPositive = number => number > 0; Console.WriteLine(isPositive(-5)); // false
3. Standardowe delegaty: Func, Action, Predicate
Func<...>
Używany dla metod, które coś przyjmują i coś zwracają.
Sygnatura:
— Ostatni typ to wartość zwracana, pozostałe przed nim to typy parametrów, np.:
Func<int, string> — przyjmuje int, zwraca string
Func
intToString = number => "Liczba: " + number; Console.WriteLine(intToString(7)); // "Liczba: 7"
Action<...>
Używany, gdy trzeba coś wykonać, ale nie trzeba nic zwracać (void).
Action
printHello = name => Console.WriteLine("Cześć, " + name + "!"); printHello("Wasylij"); // "Cześć, Wasylij!"
Predicate<T>
W istocie skrót dla Func<T, bool>. Używa się go, gdy potrzebna jest logiczna weryfikacja obiektu (true/false).
Predicate
isOdd = x => x % 2 != 0; Console.WriteLine(isOdd(3)); // true
Szybka ściąga
| Delegat | Sygnatura | Zastosowanie |
|---|---|---|
|
|
Transformacja, projekcja |
|
|
Efekty uboczne, output |
|
|
Filtrowanie, wyszukiwanie |
Jaki typ delegata wybrać dla wyrażenia lambda?
- Jeśli oczekiwane jest zwrócenie wartości, wybierz Func<...>
- Jeśli metoda nic nie zwraca (void), użyj Action<...>
- Jeśli potrzebna jest weryfikacja warunku, użyj Predicate<T>
Przykład: Filtrowanie listy
List
numbers = new List
{ 1, 2, 3, 4, 5, 6 }; // Oczekuje Predicate
List
evenNumbers = numbers.FindAll(x => x % 2 == 0); Console.WriteLine(string.Join(", ", evenNumbers)); // 2, 4, 6
4. Przydatne niuanse
Wyrażenia lambda i metody kolekcji: co jest pod maską?
Gdy wywołujesz metodę kolekcji, przekazując lambdę, np.:
var adults = users.Where(u => u.Age >= 18);
Metoda Where oczekuje argumentu typu Func<T, bool>. Czyli Twoja lambda u => u.Age >= 18 jest przez kompilator zamieniana na obiekt delegata tego typu.
Blok-schemat: jak to działa
Twoja lambda --> Kompilator C# --> Obiekt delegata (Func
) (u => u.Age >= 18) [typ znany] (Gotowy do wywołania w Where())
Szczegóły typizacji: inference typu
Zwykle typ delegata jest wyprowadzany automatycznie przez kompilator, jeśli kontekst jest jasny. Na przykład dla metody List<T>.Find oczekiwany jest Predicate<T>, i kompilator zna typ parametru z sygnatury metody.
List
words = new List
{ "one", "two", "three" }; var result = words.Find(word => word.Length == 5); // Find oczekuje Predicate
Console.WriteLine(result); // "three"
Jeśli kontekst nie jest jasny, trzeba pomóc kompilatorowi:
// Jawnie podajemy typ
Func
check = x => x > 10;
Delegaty zwracane: fabryka funkcji
Czasami metody mogą zwracać delegaty — tworzą „fabryki funkcji”. To wygodne do generowania dynamicznego zachowania.
// Funkcja zwracająca delegata (lambdę)
Func
GetMultiplier(int factor) { return x => x * factor; } var times3 = GetMultiplier(3); Console.WriteLine(times3(5)); // 15
To działa, bo lambda (x => x * factor) capture'uje zmienną factor z zewnętrznego kontekstu (closure/zamknięcie) i zwracana jest jako obiekt typu Func<int, int>.
5. Błędy i nieporozumienia z delegatami i lambdami
Niezgodność sygnatur
Kompilator nie pozwoli przypisać lambdy do delegata, jeśli parametry albo typ zwracany się nie zgadzają.
Func
f = x => "Nie można zwrócić stringa!"; // Błąd kompilacji
Błąd przy próbie użycia lambdy bez delegata
Nie można po prostu napisać lambdy i spróbować jej wywołać bez typu:
// To nie zadziała - kompilator nie może wywnioskować typu
// var myFunc = x => x * 2; // Błąd CS0815
// myFunc(10);
Żeby działało, trzeba jawnie podać typ albo zapewnić kontekst:
Func
myFunc = x => x * 2; Console.WriteLine(myFunc(10)); // 20
Pomieszanie Action, Func i Predicate
Czasem można przypadkowo wybrać niewłaściwy typ delegata, co spowoduje błąd zgodności sygnatury. Pamiętaj proste zasady: Func — gdy jest wynik, Action — gdy nie ma wyniku (void), Predicate<T> — gdy potrzebna jest odpowiedź logiczna (bool).
GO TO FULL VERSION