1. Wprowadzenie
Pracując z programami, nieuchronnie napotykasz sytuacje, gdy jedna część musi „powiadomić” inne, że wydarzyło się coś ważnego. Klasyczny przykład — użytkownik kliknął myszką i to zdarzenie wymaga obsługi. W codziennym życiu żyjemy już w świecie zdarzeń: czajnik na kuchence zapisał — usłyszałeś sygnał i pospiesznie zgasiłeś palnik. Kawa rozlała się na klawiaturę — serce zadrżało — i rzuciłeś się ratować laptopa. Programowanie rządzi się tymi samymi prawami.
Zdarzenie — to mechanizm pozwalający obiektowi‑źródłu (wydawcy) powiadomić inne obiekty (subskrybentów) o zajściu zmian lub akcji. To coś w stylu „powiadomiłem — kto słyszał, ten zareagował”.
W C# zdarzenia — specjalna konstrukcja oparta na typie delegata. Delegat określa sygnaturę callbacka — co i jak zostanie wywołane u subskrybentów. Deklaracja zdarzenia odbywa się słowem kluczowym event, a delegat — słowem kluczowym delegate.
Po co zdarzenia?
- Luźne powiązanie: Wydawca nic nie wie o subskrybentach — jedynie wysyła sygnał.
- Elastyczność architektury: Można dynamicznie dodawać i usuwać handlery, nie zmieniając kodu wydawcy.
- Skalowalność: Dodano nowego subskrybenta — i od razu zaczyna otrzymywać powiadomienia.
Klasyczny wzorzec „Wydawca‑Subskrybent”
Wyobraźmy sobie, że mamy klasę „Alarm przeciwpożarowy” (wydawca) i klasę „Człowiek w budynku” (subskrybent). Kiedy alarm zadziała, wysyła sygnał do wszystkich naraz — bez względu na to, ile osób jest w budynku i gdzie dokładnie. To jest wzorzec „Wydawca‑Subskrybent” (czyli Observer).
Wydawca nie wie, ile i jakich subskrybentów jest — po prostu powiadamia, a reszta sama subskrybuje powiadomienia albo je ignoruje.
Jak to działa w C#?
- Wydawca: definiuje zdarzenie (event), udostępnia subskrypcję/odsubskrypcję.
- Subskrybent: subskrybuje zdarzenie i implementuje handler (metoda‑handler zostanie wywołana przy wystąpieniu zdarzenia).
2. Zdarzenia w praktyce: pierwszy przykład
Przechodząc od domu do kodu, opiszmy najprostszą modelkę. Załóżmy, że mamy aplikację konsolową, gdzie obiekt‑timer co sekundę „tyka”, a różni handlerzy reagują (np. wypisują „tik” w konsoli albo liczą liczbę tików).
Krok 1. Definiujemy delegata i zdarzenie
public class SimpleTimer
{
// Zadeklarujmy delegata dla zdarzenia
public delegate void TickEventHandler(object sender, EventArgs e);
// Zdarzenie oparte na delegacie
public event TickEventHandler Tick;
public void Start(int count)
{
for (int i = 0; i < count; i++)
{
System.Threading.Thread.Sleep(1000); // symulacja tykania!
OnTick(); // wyrzuć zdarzenie!
}
}
protected virtual void OnTick()
{
// Wywołaj zdarzenie, jeśli są subskrybenci (Tick != null)
Tick?.Invoke(this, EventArgs.Empty);
}
}
Co tu się dzieje?
- Zdefiniowano delegata TickEventHandler z klasyczną sygnaturą object sender, EventArgs e.
- Zdarzenie Tick — punkt subskrypcji dla handlerów.
- Metoda Start symuluje „tykanie” i po timerze wywołuje OnTick.
- W OnTick zdarzenie wywoływane jest bezpiecznie: Tick?.Invoke(..., EventArgs.Empty).
Krok 2. Subskrypcja zdarzenia
class Program
{
static void Main()
{
var timer = new SimpleTimer();
// Subskrybujemy zdarzenie Tick
timer.Tick += Timer_Tick;
timer.Start(3);
// Można się odsubskrybować, jeśli trzeba
timer.Tick -= Timer_Tick;
}
static void Timer_Tick(object sender, EventArgs e)
{
Console.WriteLine("Tik!");
}
}
Tworzymy timer, subskrybujemy operatorem +=, potem przy każdym tiku wywoływany jest handler. Odsłonięcie — operator -=.
3. Przydatne niuanse
Dlaczego zdarzenia są lepsze niż „twarde” wywołania?
Gdyby SimpleTimer w OnTick pisał bezpośrednio do konsoli, klasa byłaby ściśle związana z konkretną akcją. Zdarzenia „uwalniają” kod: timer nie wie, co dokładnie zrobią subskrybenci — odpali rakietę, zaloguje do pliku czy wyśle e‑mail.
Ważna różnica między zdarzeniami a delegatami
- Delegat — „wskaźnik” na metodę, a zdarzenie — to delegat z ograniczeniami dostępu.
- Subskrybenci mogą tylko subskrybować/odsubskrybować; wywołać zdarzenie z zewnątrz nie można — może to zrobić tylko sam wydawca.
- Aby zadeklarować zdarzenie, dodaj modyfikator event do typu delegata — kompilator zapewni poprawny model dostępu.
Skrócona schemat pracy zdarzenia w C#
+------------------+ +------------------------------+
| | | |
| Wydawca | <------> | Subskrybent |
| (Publisher/Event)| | (Subscriber/Handler) |
| | | |
+------------------+ +------------------------------+
| 1) deklaruje zdarzenie | 2) subskrybuje je
| 3) wywołuje je | 4) implementuje handler
Kiedy używać zdarzeń?
- Trzeba powiadomić nieokreśloną liczbę słuchaczy o zajściu.
- Nie chcesz wiązać logiki akcji wewnątrz klasy‑źródła.
- UI, interakcje asynchroniczne, systemowe powiadomienia — wszystko kręci się wokół zdarzeń.
Skrócone cechy implementacji zdarzeń w C#
- Zdarzenia nie można wywołać „z zewnątrz”: tylko kod wydawcy ma prawo do Invoke.
- Subskrypcja/odsubskrypcja: operatory +=/-=; handlerów może być kilka.
- Zdarzenie to w praktyce lista delegatów: przy wystąpieniu zdarzenia wywoływane są wszystkie handlery w kolejności subskrypcji.
- Polecane delegaty: używaj EventHandler i EventHandler<TEventArgs> dla kompatybilności z ekosystemem .NET.
4. Typowe błędy początkujących
Zapominają sprawdzić, czy są subskrybenci: Tick != null. Lepiej używać bezpiecznego wywołania: Tick?.Invoke(...).
Subskrybują zdarzenie, ale nie odsubskrybowują, gdy handler już nie jest potrzebny. To może utrzymywać obiekty w pamięci i prowadzić do wycieków.
Próbują „wywołać” zdarzenie z zewnętrznej klasy — kompilator na to nie pozwoli. Nie możesz napisać czegoś w stylu game.GameOver(), jeśli to zdarzenie, a nie metoda.
Nie przestrzegają sygnatury delegata. Dla zdarzeń używaj standardowych EventHandler lub EventHandler<TEventArgs> — wtedy kod będzie kompatybilny z innymi bibliotekami .NET.
GO TO FULL VERSION