Menu

Delegaty w C#: deklaracja, wywołanie, multicast i callbacki

Czym jest delegat w C# i jak go używać: deklarowanie typu delegatu, tworzenie go z metody, lambdy lub metody anonimowej, wywoływanie, delegaty jako callbacki, delegaty multicast z += i -=, sprawdzanie null przez ?.Invoke oraz związek delegatów z Func, Action i zdarzeniami.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Delegat to typ, który reprezentuje sygnaturę metody. Zmienna typu delegatu przechowuje referencję do metody o tej sygnaturze, a wywołanie zmiennej wywołuje metodę. Tak C# traktuje funkcje jako wartości: możesz je przechowywać, przekazywać do innych metod i wywoływać później.

Wynik:

40
70
68.00

delegate decimal PriceRule(decimal price); deklaruje nowy typ, tak samo jak robi to class. Pasuje do niego każda metoda, która przyjmuje jeden decimal i zwraca decimal. Zmienna rule wskazywała po kolei trzy różne metody, a każde wywołanie uruchamiało tę, którą akurat przechowywała.

Deklarowanie, tworzenie i wywoływanie

Deklaracja typu delegatu wygląda jak sygnatura metody poprzedzona słowem kluczowym delegate. Zwykle umieszcza się ją na poziomie przestrzeni nazw, obok klas, albo zagnieżdża wewnątrz klasy.

Instancję delegatu można utworzyć na cztery sposoby:

PriceRule a = HalfPrice;                             // method group (most common)
PriceRule b = new PriceRule(HalfPrice);              // explicit constructor, same thing
PriceRule c = p => p - 5m;                            // lambda expression
PriceRule d = delegate (decimal p) { return p; };    // anonymous method (C# 2 syntax)

Wywołać ją można na dwa sposoby, które są identyczne: rule(80m) i rule.Invoke(80m). Kompilator sprawdza sygnaturę przy przypisaniu, więc niepasująca metoda to błąd kompilacji, a nie niespodzianka w czasie działania.

Delegat może też wskazywać metodę instancyjną. Zapamiętuje wtedy zarówno metodę, jak i obiekt, na którym ma ją wywołać:

Wynik:

Ana got: Your order shipped
target: Mailbox, method: Receive

Target to obiekt, na którym delegat wywoła metodę (null dla metody statycznej), a Method opisuje metodę. Przechowywanie obiektu docelowego oznacza też, że delegat utrzymuje ten obiekt przy życiu, co ma znaczenie przy zdarzeniach.

Delegaty jako callbacki

Główne zadanie delegatu to pozwolić metodzie wywołać kod wybrany przez jej kod wywołujący. Metoda określa, kiedy coś się dzieje; kod wywołujący określa co:

Wynik:

[##        ] 25%
[#####     ] 50%
[#######   ] 75%
[##########] 100%
import finished

ImportRows nic nie wie o konsolach ani paskach. Ta sama metoda może sterować paskiem postępu, linią w logu albo aktualizacją UI, zależnie od tego, co przekaże kod wywołujący. List.Sort(Comparison<T>), Array.Find(Predicate<T>), Task.Run(Action) i każdy operator LINQ działają według tego wzorca.

Delegaty multicast: += i -=

Delegat może przechowywać więcej niż jedną metodę. += dopisuje metodę do jego listy wywołań, -= ją usuwa, a wywołanie delegatu wywołuje każdą metodę po kolei:

Wynik:

email sent for A-1001
stock updated for A-1001
warehouse notified for A-1001
3 handlers
email sent for A-1002
warehouse notified for A-1002

W praktyce liczą się trzy szczegóły delegatów multicast:

  • Wartości zwracane: jeśli typ delegatu zwraca wartość, wywołanie delegatu multicast zwraca tylko wynik ostatniej metody. Żeby zebrać wszystkie wyniki, przejdź pętlą po GetInvocationList() i wywołaj każdą metodę samodzielnie.
  • Wyjątki: jeśli jedna metoda rzuci wyjątek, pozostałe się nie wykonają, a wyjątek trafi do kodu wywołującego.
  • Niemodyfikowalność: delegaty są niemodyfikowalne. += tworzy nowy delegat z dłuższą listą i przypisuje go z powrotem do zmiennej, dlatego usunięcie przez -= na kopii nie wpływa na oryginał.

Usunięcie lambdy przez -= działa tylko wtedy, gdy przekażesz tę samą instancję delegatu. Dwukrotne napisanie tej samej lambdy tworzy dwa różne delegaty, więc handlers -= id => Log(id); niczego nie usuwa. Jeśli planujesz anulować subskrypcję, zapisz lambdę w zmiennej.

Delegaty null i ?.Invoke

Zmienna delegatu, do której nic nie przypisano, ma wartość null, a usunięcie ostatniej metody przez -= też zostawia null. Wywołanie null rzuca NullReferenceException:

Wynik:

no handler, no crash
saved draft.txt
NullReferenceException

?.Invoke to standardowy sposób wywołania opcjonalnego callbacku. Operatora ?. nie da się postawić bezpośrednio przed nawiasami (onSaved?("x") to niepoprawna składnia), więc wzorzec wywołuje metodę Invoke delegatu z nazwy, co działa tak samo jak skrócone wywołanie.

Delegaty, Func, Action i zdarzenia

Deklarowanie typu delegatu dla każdej sygnatury byłoby żmudne, dlatego .NET dostarcza typy generyczne:

Typ wbudowanyOdpowiednik we własnej deklaracji
Actiondelegate void Action();
Action<T>delegate void Action<T>(T arg);
Func<TResult>delegate TResult Func<TResult>();
Func<T, TResult>delegate TResult Func<T, TResult>(T arg);
Predicate<T>delegate bool Predicate<T>(T obj);

PriceRule z pierwszego przykładu ma ten sam kształt co Func<decimal, decimal>, a mimo to są to różne typy, które nie konwertują się jeden na drugi. Nie konwertują się nawet dwa typy wbudowane o identycznych sygnaturach:

Func<int, bool> isPositive = x => x > 0;
Predicate<int> p = isPositive;
// error CS0029: Cannot implicitly convert type 'System.Func<int, bool>' to 'System.Predicate<int>'

Predicate<int> ok = new Predicate<int>(isPositive);   // wrap it explicitly
Predicate<int> ok2 = x => isPositive(x);              // or with a lambda

Lambda konwertuje się na dowolny zgodny typ delegatu, więc API zwykle przyjmują lambdy i ten problem w ogóle się nie pojawia. Strona o Func i Action omawia rodzinę typów generycznych i to, kiedy własna deklaracja nadal ma sens.

Zdarzenia są zbudowane bezpośrednio na delegatach. Pole event to delegat multicast, na którym kod zewnętrzny może wykonać tylko += i -=, ale nigdy go nie wywoła ani nie nadpisze. Lista subskrybentów z przykładu multicast powyżej to dokładnie to, czym zdarzenie zarządza za ciebie, z tą dodatkową ochroną. Zdarzenia mają własną stronę.

Najczęściej zadawane pytania

Czym jest delegat w C#?

Delegat to bezpieczna typowo referencja do metody. Deklarujesz typ delegatu z sygnaturą, na przykład delegate decimal PriceRule(decimal price);, a każdą metodę o tej sygnaturze (statyczną, instancyjną albo lambdę) można zapisać w zmiennej tego typu i później przez nią wywołać. W ten sposób C# przekazuje funkcje jako wartości.

Czym różni się delegat od Func w C#?

Func<T, TResult> i Action<T> to typy delegatów, które .NET już deklaruje za ciebie, generyczne względem typów parametrów i wartości zwracanej. Własna deklaracja delegate nadaje typowi nazwę i nazwy parametrów, które dokumentują jego rolę, oraz obsługuje parametry ref, out i in. W większości kodu Func i Action wystarczą.

Czym jest delegat multicast w C#?

To delegat, który przechowuje kilka metod. += dodaje metodę do jego listy wywołań, a -= ją usuwa; wywołanie delegatu wywołuje je wszystkie po kolei. Jeśli delegat zwraca wartość, kod wywołujący dostaje tylko wynik ostatniej metody, a wyjątek w jednej metodzie zatrzymuje wykonanie pozostałych.

Jak bezpiecznie wywołać delegat, który może być null?

Użyj operatora warunkowego null: onProgress?.Invoke(50);. Zmienna delegatu bez przypisanych metod ma wartość null, a bezpośrednie jej wywołanie rzuca NullReferenceException. ?.Invoke odczytuje zmienną raz, więc jest bezpieczne także wtedy, gdy inny wątek w tym samym momencie usuwa ostatnią metodę obsługi.

Kiedy deklarować własny typ delegatu zamiast używać Func lub Action?

Gdy sygnatura potrzebuje parametrów ref, out lub in (Func i Action nie potrafią ich wyrazić), gdy nazwany typ czyni publiczne API czytelniejszym (delegate bool Validator(string input)) albo gdy chcesz, żeby nazwy parametrów pokazywały się w IntelliSense. W pozostałych przypadkach wybieraj Func i Action, które rozpozna każdy programista .NET.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ