CodeGym /Kursy /C# SELF /Delegaty jako typ dla wyrażeń lambda

Delegaty jako typ dla wyrażeń lambda

C# SELF
Poziom 50 , Lekcja 0
Dostępny

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
Action
void()
Action act = () => ...
Func<int>
int()
Func<int> f = () => 42
Func<int, bool>
bool(int)
Func<int, bool> p = x => x > 0

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
Func<T1, TResult>
Result(T1)
Transformacja, projekcja
Action<T1>
void(T1)
Efekty uboczne, output
Predicate<T>
bool(T)
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).

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