Delegaty w C# — czym są i jak działają

Delegat to typ, którego zmienna nie przechowuje liczby ani obiektu — przechowuje odwołanie do metody. Skoro zmienna wskazuje na metodę, można ją wywołać dokładnie tak, jak samą metodę: nawiasami, opcjonalnie z argumentami. To fundament, na którym zbudowane są zdarzenia (events) oraz generyczne delegaty Func i Action — ten artykuł skupia się na samym słowie kluczowym delegate i własnych, nazwanych typach delegatów.
Deklaracja własnego typu delegata
Podobnie jak słowo kluczowe class tworzy typ opisujący dane i zachowanie, słowo kluczowe delegate tworzy typ opisujący sygnaturę metody — jaki zwraca wynik i jakie przyjmuje parametry. Każda metoda o pasującej sygnaturze może zostać przypisana do zmiennej tego typu, niezależnie od jej nazwy.
// Deklaracja typu delegata — metody void, przyjmujące jeden string
public delegate void Writer(string message);To nie jest wywołanie metody — nie ma tu nawiasów klamrowych ani ciała. To definicja typu, dokładnie jak definicja klasy, tylko krótsza. Teraz możemy stworzyć zmienną tego typu i przypisać jej dowolną metodę o zgodnej sygnaturze:
public class Logger
{
public void WriteMessage(string message)
{
Console.WriteLine($"[LOG] {message}");
}
}
var logger = new Logger();
// Tworzymy delegat i przypisujemy mu metodę WriteMessage
Writer writer = logger.WriteMessage;
// Wywołujemy delegat — to wywołuje logger.WriteMessage("...") pod spodem
writer("Aplikacja uruchomiona");
// [LOG] Aplikacja uruchomionaKluczowa rzecz do zapamiętania: logger.WriteMessage bez nawiasów nie wywołuje metody — to przekazanie odwołania do niej. Wywołanie następuje dopiero, gdy użyjesz zmiennej writer z nawiasami. Nazwa metody (WriteMessage) nie musi się niczym wiązać z nazwą typu delegata (Writer) — liczy się wyłącznie zgodność sygnatury: taki sam typ zwracany i takie same parametry.
Delegaty multicast — łączenie wielu metod
To, czego nie robią pojedyncze wywołania metod, a robią delegaty: można połączyć wiele metod w jednej zmiennej operatorem +=, a wywołanie zmiennej uruchomi wszystkie po kolei:
Writer writer = logger.WriteMessage;
writer += message => File.AppendAllText("log.txt", message + Environment.NewLine);
writer += message => Console.Beep();
writer("Krytyczny błąd systemu");
// wykonuje WriteMessage, potem zapis do pliku, potem sygnał dźwiękowyTo właśnie mechanizm multicast stoi za zdarzeniami z wieloma subskrybentami — event to w gruncie rzeczy delegat multicast z ograniczonym dostępem z zewnątrz klasy (szczegóły w artykule o events). Operator -= działa symetrycznie i usuwa metodę z listy wywołań.
Pułapka: jeśli delegat multicast zwraca wartość (nie void), wywołanie zwróci tylko wynik ostatniej wywołanej metody — wyniki pośrednie są tracone. Z tego powodu delegaty multicast używa się niemal wyłącznie z metodami void (jak w zdarzeniach).
Czego Func/Action nie potrafią, a własny delegat tak
Skoro .NET ma gotowe, generyczne Func<> i Action<>, po co w ogóle definiować własny typ delegata? Dwa konkretne powody:
- Parametry ref/out/in —
FunciActionnie obsługują modyfikatorówref,outaniin. Jeśli metoda musi przyjmować parametr przez referencję, potrzebujesz własnego typu delegata.
// Func/Action nie da się tak zapisać — trzeba własnego delegata
public delegate bool TryParser<T>(string input, out T result);- Nazwana, domenowa intencja —
public delegate void OrderProcessed(Order order);czyta się i dokumentuje lepiej niżAction<Order>rozsiane po całym kodzie. Nazwany typ pojawia się też czytelnie w IntelliSense i sygnaturach metod publicznego API biblioteki.
Poza tymi dwoma przypadkami współczesny C# niemal zawsze preferuje gotowe Func/Action — mniej kodu do utrzymania, ten sam efekt. Własny delegate to dziś raczej świadomy wyjątek niż domyślny wybór.
Delegat jako parametr metody
Najczęstsze praktyczne zastosowanie delegata — przekazanie zachowania jako argumentu, żeby metoda ogólna mogła wywołać konkretną logikę dostarczoną przez wywołującego:
public delegate bool Predicate(int number);
static List<int> Filter(List<int> numbers, Predicate condition)
{
var result = new List<int>();
foreach (var number in numbers)
{
if (condition(number))
{
result.Add(number);
}
}
return result;
}
var parzyste = Filter(new List<int> { 1, 2, 3, 4, 5, 6 }, n => n % 2 == 0);
// [2, 4, 6]To dokładnie ten sam wzorzec, na którym opiera się List<T>.Where(Func<T, bool> predicate) w LINQ — tam .NET używa gotowego Func zamiast własnego typu, bo nie potrzebuje ref/out ani domenowej nazwy.
FAQ
Czy delegat może wskazywać na metodę statyczną?
Tak — składnia jest identyczna, np. Writer writer = Logger.StatycznaWriteMessage;, bez potrzeby instancji obiektu.
Co się stanie, gdy wywołam pusty (null) delegat?NullReferenceException — dokładnie tak samo jak w przypadku zdarzeń. Sprawdzaj null przed wywołaniem albo użyj operatora ?.Invoke(...).
Czy Func/Action to też delegaty?
Tak — to gotowe, generyczne typy delegatów dostarczone przez .NET, żeby nie trzeba było deklarować własnych dla najczęstszych sygnatur. Pełne omówienie w artykule o Func i Action.
Jak to się ma do wzorców projektowych?
Delegaty to fundament, na którym C# buduje Observer Pattern — event to w praktyce gotowy, wbudowany w język Observer, bez ręcznie pisanej listy obserwatorów i interfejsu.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
2 comments
Dodaj komentarz
Musisz się zalogować, aby móc dodać komentarz.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →
Nie wiem czy wiesz ale tworzysz bardzo ciekawe i inspirujące treści. Trzymaj dalej tak wysoki poziom, a już wkrótce osiągniesz szczyty wyszukiwarek na topowe frazy 🙂