🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

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

Delegaty w C# — przypisanie metody do zmiennej 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.

Wypróbuj

Trzy kroki krok po kroku

public delegate void ButtonAction();ButtonAction actionOnX = Jump;actionOnX();actionOnX = Fire;actionOnX();
Krok 1 · Definiujesz

Powstaje typ, nie metoda. Opis kształtu: nic nie przyjmuje, nic nie zwraca.

Krok 2 · Przypisujesz

Bez nawiasów. Do zmiennej wpada sama metoda — nic się jeszcze nie wykonuje.

Krok 3 · Wywołujesz

Z nawiasami. Dopiero teraz coś się dzieje.

Konsola — naciśnij „Następny krok”…

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 ifswitch 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 edycji if/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łuje

Invoke() 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

ZapisWynikDlaczego
ButtonAction a = Jump;przypisanie metody, bez wywołania
ButtonAction a = Jump();❌ CS0029Jump() zwraca void, nie ButtonAction
ButtonAction a = Crouch;❌ CS0123Crouch(int) ma parametr, delegat nie
ButtonAction a = IsGrounded;❌ CS0407zwraca bool zamiast void
a(); przy a == null❌ NullReferenceExceptiondelegat to typ referencyjny
a?.Invoke();jedyny bezpieczny sposób wywołania
Sprawdź się

Czy ta metoda pasuje do delegata?

Kliknij kandydata. Jeśli nie pasuje — zobaczysz dokładnie ten komunikat, który pokaże Ci kompilator.

Szablon — public delegate void ButtonAction();
void ( )
Wybierz metodę powyżej…
Sprawdzone: 0 z 5

Co kompilator generuje pod maską

Ta jedna linijka:

public delegate void ButtonAction();

zamienia się w pełnoprawną klasę: 
sealed class ButtonAction : MulticastDelegate,
z metodami InvokeBeginInvoke 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 MulticastDelegate z polami Method i Target.

Quiz · 4 pytania

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ż

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

Dodaj komentarz

czytanie to początek

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ę →