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,
Kliknietoma wartośćnull, aKliknieto.Invoke(...)rzuciNullReferenceException. 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.
🚀 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ń.
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ę →