Delegaty w C# — dlaczego metodę przypisujesz bez nawiasów

public static void Main()
{
Console.WriteLine("--- Tryb Eksploracji ---");
Console.Write("Naciskasz [X]: ");
Jump();
Console.WriteLine("\n--- Tryb Walki ---");
Console.Write("Naciskasz [X]: ");
Fire();
}
private static void Jump()
{
Console.WriteLine("🦘 Postać podskakuje w górę!");
}
private static void Fire()
{
Console.WriteLine("💥 Postać oddaje strzał z pistoletu!");
}Przycisk X na padzie w trybie eksploracji każe postaci skoczyć, a w trybie walki strzelić. Ten sam przycisk, dwa zachowania. Powyższy kod jest jednak przybity na sztywno do konkretnych metod: dodanie trybu skradania oznacza edycję Main(), czyli zmianę kodu, który już działa i był przetestowany.
Delegat odwraca tę zależność — przycisk przestaje wiedzieć, co robi, a zaczyna jedynie wywoływać to, co ktoś pod niego podłożył.
Delegat to zmienna, w której leży metoda
int przechowuje liczbę, string — tekst, a delegat metodę. I tak samo jak liczbę, można ją podmienić w trakcie działania programu.
public delegate void ButtonAction();Ta linijka wygląda jak deklaracja metody, ale nią nie jest – nie ma ciała ani klamer. To definicja typu, stojąca obok klasy tak samo jak enum czy interface:
utwórz typ ButtonAction, którego zmienne przechowają dowolną metodę bez argumentów i bez zwracanej wartości.
Jump() i Fire() z pierwszego listingu mają dokładnie taki kształt – void i puste nawiasy. To jedyne, co liczy się przy dopasowaniu.
Zasada: definicja delegata opisuje kształt metody, nie konkretną metodę.
Trzy kroki, które powtarzasz zawsze
Praca z delegatem wygląda tak samo w konsolowej zabawce i w silniku reguł biznesowych: definiujesz szablon, przypisujesz metodę, wywołujesz.
Trzy kroki krok po kroku
Powstaje typ, nie metoda. Opis kształtu: nic nie przyjmuje, nic nie zwraca.
Bez nawiasów. Do zmiennej wpada sama metoda — nic się jeszcze nie wykonuje.
Z nawiasami. Dopiero teraz coś się dzieje.
Zwróć uwagę: linijka actionOnX(); jest w krokach 3 i 5
identyczna. Zmieniło się wyłącznie to, co siedzi w zmiennej — i dlatego nie potrzebujesz
tu ani jednego if.
using System;
// KROK 1: Definicja delegata — typ opisujący KSZTAŁT metody.
public delegate void ButtonAction();
public class Program
{
public static void Main()
{
// KROK 2: Do zmiennej wkładamy metodę Jump — BEZ nawiasów.
ButtonAction actionOnX = Jump;
Console.WriteLine("--- Tryb Eksploracji ---");
Console.Write("Naciskasz [X]: ");
// KROK 3: wywołujesz Jump()
actionOnX();
// Pod TĘ SAMĄ zmienną podpinamy inną metodę.
actionOnX = Fire;
Console.WriteLine("\n--- Tryb Walki ---");
Console.Write("Naciskasz [X]: ");
// ta sama linijka, inny efekt
actionOnX();
}
private static void Jump()
{
Console.WriteLine("🦘 Postać podskakuje w górę!");
}
private static void Fire()
{
Console.WriteLine("💥 Postać oddaje strzał z pistoletu!");
}
}Zwróć uwagę na to, czego tu nie ma: żadnego if, switch ani flagi z trybem gry. Dodanie trybu skradania to nowa metoda i jedno przypisani, bez dotykania linijki actionOnX(). To zasada open/closed z SOLID bez ani jednego interfejsu; ta sama obserwacja stoi za wzorcem Strategy.
Zauważ też, że actionOnX wygląda jak metoda, choć jest zmienną, możesz ją nadpisać, sprawdzić pod kątem null, a docelowo przekazać do innej metody jak każdy argument.
Zasada:
jeśli podmiana zachowania wymaga edycjiif/switch, masz kandydata na delegat.
Pułapka #1: nawiasy przy przypisaniu (CS0029)
Najczęstszy błąd przy pierwszym kontakcie z delegatami:
ButtonAction actionOnX = Jump(); // ❌
// CS0029: Cannot implicitly convert type 'void' to 'ButtonAction'
ButtonAction actionOnX = Jump; // ✅Komunikat jest uczciwy: nawiasy oznaczają: „wykonaj teraz i daj wynik", więc kompilator wykonał Jump(), dostał void, czyli nic i próbuje to nic przypisać do zmiennej. Wersja z nawiasami bywa poprawna, ale tylko gdy metoda zwraca delegat; błędem nie są same nawiasy, lecz niezgodność typu wyniku.
Zasada:
bez nawiasów, daj mi samą metodę. Z nawiasami, wykonaj ją i daj wynik.
Pułapka #2: niezainicjalizowany delegat to null
Delegat jest typem referencyjnym - nie ma wartości domyślnej w rodzaju zera dla int. Pusta zmienna to null, a jej wywołanie kończy się wyjątkiem w runtime, nie błędem kompilacji. To najpodstępniejsza z trzech pułapek: przechodzi kompilację i testy, w których delegat zawsze jest ustawiony.
ButtonAction? actionOnY = null;
actionOnY(); // ❌ NullReferenceException — RUNTIME, nie kompilacja
actionOnY?.Invoke(); // ✅ sprawdza null, dopiero potem wywołujeInvoke() to jawna nazwa metody wywołującej delegat,actionOnX() jest lukrem składniowym na dokładnie to samo, więc obie linie poniżej są równoważne:
actionOnX.Invoke();
actionOnX();Przy zdarzeniach ?.Invoke() ma jeszcze drugie zastosowanie, chroni przed wyścigiem wątków między sprawdzeniem null a wywołaniem.
Zasada:
delegat spoza Twojej metody wywołuj przez?.Invoke(), nie przez().
Pułapka #3: liczy się sygnatura, nie nazwa
Dopasowanie odbywa się po całej sygnaturze:
liczbie parametrów, ich typach i typie zwracanym. Nazwa metody nie liczy się wcale.
// Świetna nazwa dla akcji na padzie — ale ZŁY KSZTAŁT.
private static void Crouch(int height)
=> Console.WriteLine($"Kucanie na wysokość {height}.");
// Nic nie przyjmuje, ale ZWRACA bool — też nie pasuje.
private static bool IsGrounded() => true;ButtonAction bad3 = Crouch;
// ❌ CS0123: No overload for 'Crouch' matches delegate 'ButtonAction'
ButtonAction bad4 = IsGrounded;
// ❌ CS0407: 'bool IsGrounded()' has the wrong return type
Dwa błędy z dwóch różnych powodów: Crouch przyjmuje int, a ButtonAction nie deklaruje parametrów; IsGrounded nie przyjmuje nic, ale zwraca bool zamiast void.
To przewaga delegatów nad refleksją czy dynamic - kontrakt weryfikuje kompilator, więc o niedopasowaniu dowiadujesz się w edytorze, a nie od użytkownika na produkcji.
Zasada:
czytaj numer błędu, nie tylko treść. CS0123 to niezgodne parametry, CS0407 to niezgodny typ zwracany.
Ściągawka na code review
| Zapis | Wynik | Dlaczego |
|---|---|---|
ButtonAction a = Jump; | ✅ | przypisanie metody, bez wywołania |
ButtonAction a = Jump(); | ❌ CS0029 | Jump() zwraca void, nie ButtonAction |
ButtonAction a = Crouch; | ❌ CS0123 | Crouch(int) ma parametr, delegat nie |
ButtonAction a = IsGrounded; | ❌ CS0407 | zwraca bool zamiast void |
a(); przy a == null | ❌ NullReferenceException | delegat to typ referencyjny |
a?.Invoke(); | ✅ | jedyny bezpieczny sposób wywołania |
Czy ta metoda pasuje do delegata?
Kliknij kandydata. Jeśli nie pasuje — zobaczysz dokładnie ten komunikat, który pokaże Ci kompilator.
Co kompilator generuje pod maską
Ta jedna linijka:
public delegate void ButtonAction();zamienia się w pełnoprawną klasę: sealed class ButtonAction : MulticastDelegate,
z metodami Invoke, BeginInvoke i EndInvoke.
Stąd delegat jest typem referencyjnym i dlatego może być null.
Wewnątrz siedzą dwa pola: Method mówi co zostanie wywołane, a Target — na czym.
Tutaj Target jest null, bo Jump i Fire są statyczne. Przy metodach instancyjnych to właśnie Target trzyma silną referencję do obiektu i odpowiada za większość wycieków pamięci w aplikacjach opartych na zdarzeniach.
Gdzie już ich używasz, nawet o tym nie wiedząc
Własny delegate piszesz dziś rzadko, prawie zawsze wystarczą gotowe Func i Action, a mechanizm pod spodem jest identyczny.
Każda lambda w LINQ to delegat. Każdy += przy zdarzeniu to delegat. Pipeline w ASP.NET Core to łańcuch delegatów asynchronicznych, a Observer jest wbudowany w język dzięki nim. To nie ciekawostka na rozmowę rekrutacyjną, to fundament połowy kodu, który piszesz na co dzień.
Podsumowanie
- Delegat to typ, którego zmienne przechowują metody
- definiujesz go raz, obok klasy. - Przypisujesz bez nawiasów, wywołujesz z nawiasami.
- Dopasowanie działa po całej sygnaturze, nie po nazwie metody.
- Kontrakt sprawdza kompilator
- CS0029, CS0123 i CS0407 wyłapiesz w edytorze. - Delegat to typ referencyjny, więc pusty jest
null
- wywołuj przez?.Invoke(). - Pod maską to klasa po
MulticastDelegatez polamiMethodiTarget.
Sprawdź, czy to naprawdę weszło
Bez logowania, bez wysyłania — wszystko dzieje się w Twojej przeglądarce.
🎥 Wersja wideo: ten sam przykład krok po kroku - Delegaty w C# #1 — Przycisk na padzie
📘 Darmowa roadmapa Junior .NET Developer: dev-hobby.pl/lista-vip
Zadanie na 5 minut:
dodaj tryb skradania z akcją Sneak().
Potem celowo zepsuj kod trzy razy - nawiasy przy przypisaniu, parametr int, typ zwracany bool - i przeczytaj wszystkie komunikaty.
Który Cię zaskoczył? Napisz w komentarzu.
Zobacz też
- Func i Action w C# — gotowe delegaty zamiast własnych
- Zdarzenia (Events) w C# — delegat ze słowem kluczowym
event - Observer Pattern w C# — wzorzec wbudowany w język
- LINQ w C# — kompletny przewodnik — każda lambda to delegat
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ę →