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

Zdarzenia (Events) w C# — jak działają

Zdarzenia (events) pozwalają obiektowi ogłosić: “stało się coś ciekawego” — bez wiedzy o tym, kto (jeśli ktokolwiek) tego słucha. Przycisk na ekranie nie wie i nie musi wiedzieć, ile fragmentów kodu reaguje na jego kliknięcie. Ten mechanizm nazywa się wzorcem publisher/subscriber: obiekt publikujący zdarzenie (np. przycisk) ogłasza jego zajście, a kod subskrybujący (np. Twoja logika biznesowa) rejestruje się, by zostać powiadomionym.

Zdarzenia w C# są zbudowane na bazie delegatów — jeśli nie znasz jeszcze tego mechanizmu, zajrzyj najpierw do artykułu o delegatach w C#. Tutaj skupiamy się wyłącznie na tym, co słowo kluczowe event dodaje ponad zwykły delegat — i dlaczego to dodanie ma znaczenie.

Najprostsze zdarzenie — składnia

public class Przycisk
{
    // Deklarujemy zdarzenie oparte na wbudowanym delegacie EventHandler
    public event EventHandler? Kliknieto;

    public void SymulujKlikniecie()
    {
        // Wywołujemy zdarzenie, jeśli ktoś nasłuchuje
        Kliknieto?.Invoke(this, EventArgs.Empty);
    }
}
var przycisk = new Przycisk();

// Subskrypcja zdarzenia operatorem +=
przycisk.Kliknieto += (sender, args) =>
{
    Console.WriteLine("Przycisk został kliknięty!");
};

przycisk.SymulujKlikniecie(); // "Przycisk został kliknięty!"

EventHandler to wbudowany w .NET typ delegata o sygnaturze void (object? sender, EventArgs e) — standard, którego trzyma się zdecydowana większość zdarzeń w bibliotece klas .NET. sender to obiekt, który wywołał zdarzenie (przydatne, gdy jeden handler obsługuje wiele instancji), a EventArgs niesie dodatkowe dane o zdarzeniu.

Własne dane zdarzenia — generyczny EventHandler<T>

Gdy subskrybent potrzebuje więcej informacji niż samo “coś się stało”, używamy generycznej wersji EventHandler<TEventArgs> z własną klasą argumentów dziedziczącą po EventArgs:

public class ZamowienieZlozoneEventArgs : EventArgs
{
    public string NumerZamowienia { get; }
    public decimal Kwota { get; }

    public ZamowienieZlozoneEventArgs(string numer, decimal kwota)
    {
        NumerZamowienia = numer;
        Kwota = kwota;
    }
}

public class SklepInternetowy
{
    public event EventHandler<ZamowienieZlozoneEventArgs>? ZamowienieZlozone;

    public void ZlozZamowienie(string numer, decimal kwota)
    {
        // ...logika zapisu zamówienia...

        ZamowienieZlozone?.Invoke(this, new ZamowienieZlozoneEventArgs(numer, kwota));
    }
}
var sklep = new SklepInternetowy();

sklep.ZamowienieZlozone += (sender, e) =>
{
    Console.WriteLine($"Nowe zamówienie {e.NumerZamowienia} na kwotę {e.Kwota} zł");
};

sklep.ZlozZamowienie("FV/1/2026", 149.99m);

Wielu subskrybentów jednocześnie

Do jednego zdarzenia można podpiąć dowolną liczbę niezależnych fragmentów kodu — każdy zostanie wywołany po kolei, w kolejności subskrypcji:

sklep.ZamowienieZlozone += (s, e) => ZapiszDoBazy(e);
sklep.ZamowienieZlozone += (s, e) => WyslijEmailPotwierdzajacy(e);
sklep.ZamowienieZlozone += (s, e) => ZaktualizujStatystyki(e);

Piękno tego wzorca: klasa SklepInternetowy nie wie i nie musi wiedzieć, ile ma subskrybentów ani co oni robią — publikuje zdarzenie i kończy swoją odpowiedzialność. To realizacja zasady pojedynczej odpowiedzialności (SRP) i luźnego powiązania (loose coupling) między komponentami.

Dlaczego event, a nie zwykłe pole delegata?

To pytanie pada często — skoro zdarzenie to “tylko” delegat, po co osobne słowo kluczowe? Odpowiedź: enkapsulacja. Gdyby Kliknieto było zwykłym publicznym polem typu EventHandler, każdy kod z zewnątrz klasy mógłby nie tylko subskrybować (+=), ale też:

  • nadpisać wszystkich subskrybentów przez zwykłe przypisanie = zamiast +=,
  • samodzielnie wywołać zdarzenie z zewnątrz klasy, np. przycisk.Kliknieto.Invoke(...), udając kliknięcie, które nigdy nie nastąpiło.

Słowo kluczowe event ogranicza dostęp z zewnątrz klasy wyłącznie do += i -= — wywołanie (Invoke) oraz bezpośrednie przypisanie są możliwe tylko wewnątrz klasy, która zdarzenie deklaruje. To różnica koncepcyjnie podobna do właściwości z prywatnym setterem: kontrolujesz, kto i jak może zmienić stan.

Częste pułapki

  • Wywołanie zdarzenia bez sprawdzenia null — jeśli nikt nie subskrybuje, Kliknieto ma wartość null, a Kliknieto.Invoke(...) rzuci NullReferenceException. Operator ?. (jak w przykładach wyżej) rozwiązuje to bezpiecznie w jednej linijce.
  • Zapominanie o odsubskrybowaniu (-=) — jeśli subskrybent żyje krócej niż publisher (np. formularz subskrybuje zdarzenie długo żyjącego serwisu), a nigdy się nie wypisze, obiekt publishera będzie trzymał referencję do subskrybenta w nieskończoność — klasyczny wyciek pamięci w aplikacjach z UI.
  • Wyjątek w jednym subskrybencie przerywa pozostałych — jeśli pierwszy z kilku handlerów rzuci wyjątek, kolejne w ogóle się nie wykonają. W kodzie produkcyjnym z wieloma subskrybentami warto rozważyć opakowanie wywołania każdego handlera w osobny try/catch.

FAQ

Czy event musi być oparty na EventHandler?
Nie — możesz zadeklarować zdarzenie na bazie dowolnego typu delegata, także własnego. EventHandler/EventHandler<T> to konwencja .NET, warta trzymania się dla spójności z resztą ekosystemu.

Czym różni się event od delegata?
Event to delegat z ograniczonym dostępem — z zewnątrz klasy można go tylko subskrybować/odsubskrybować (+=/-=), nie wywołać ani nadpisać. Pełne omówienie samych delegatów znajdziesz w dedykowanym artykule.

Kiedy używać event, a kiedy Func/Action?
event pasuje do modelu “ogłoszenie do wielu niezależnych, nieznanych z góry słuchaczy” (UI, powiadomienia domenowe). Func/Action (opisane w osobnym artykule) lepiej pasują, gdy przekazujesz pojedynczy, konkretny fragment logiki jako parametr metody.

Czy to jest jakiś znany wzorzec projektowy?
Tak — event to natywna implementacja Observer Pattern w C#. Pełny rozkład wzorca i kiedy warto sięgnąć po klasyczny interfejs zamiast eventu — w dedykowanym artykule.

👨‍💻
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ć.

🚀 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.

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