1. Składnia subskrypcji zdarzenia
Wyobraź sobie aplikację do zarządzania zadaniami: gdy zadanie zostanie ukończone, wyzwalane jest zdarzenie, a handler może np. wysłać powiadomienie, zaktualizować UI albo zapisać informację do loga. C# robi subskrypcję zdarzeń wygodną i bezpieczną: wyraźnie wskazujesz, że konkretny handler reaguje na konkretne zdarzenie.
Subskrypcja zdarzenia w C# — to prawie jak dopisanie się na listę gości na imprezę:
publisher.MyEvent += HandlerMethod;
Tutaj publisher — obiekt, który zadeklarował zdarzenie MyEvent, a HandlerMethod — metoda, która zostanie wywołana, gdy zdarzenie wystąpi.
Zobaczmy to w kontekście minimalnego przykładu. Załóżmy, że mamy aplikację do liczenia kliknięć:
public class Clicker
{
public event Action Clicked;
public void Click()
{
// Coś zostało kliknięte!
Clicked?.Invoke();
}
}
To nasz "wydawca" zdarzenia Clicked. Teraz — zasubskrybujemy handler:
Clicker clicker = new Clicker();
void OnClicked()
{
Console.WriteLine("Przycisk został naciśnięty!");
}
clicker.Clicked += OnClicked;
// Gdzieś w kodzie
clicker.Click();
// → "Przycisk został naciśnięty!"
Jak to działa? Do zdarzenia "klik" dodaliśmy naszą metodę OnClicked, i będzie ona wywoływana za każdym razem, gdy nastąpi klik.
2. Handlery zdarzeń: jakie są i jak je deklarować
Handler zdarzenia — to metoda, która zostanie wywołana, gdy zdarzenie wystąpi. Jej sygnatura musi odpowiadać typowi delegata zdarzenia. Na przykład, jeśli mamy zdarzenie zadeklarowane jako public event Action Clicked;, to handler powinien być metodą bez parametrów i bez wartości zwracanej.
Handler dla Action
void OnClicked()
{
Console.WriteLine("Zdarzenie wystąpiło (Action)!");
}
Handler dla standardowego EventHandler
Kiedy używasz klasycznego podejścia z EventHandler, handler przyjmuje dwa parametry: nadawcę (object sender) i dane zdarzenia (EventArgs e):
public class Alarm
{
public event EventHandler AlarmRaised;
public void RaiseAlarm()
{
AlarmRaised?.Invoke(this, EventArgs.Empty);
}
}
Alarm alarm = new Alarm();
void AlarmHandler(object sender, EventArgs e)
{
Console.WriteLine("Włączono alarm!");
}
alarm.AlarmRaised += AlarmHandler;
alarm.RaiseAlarm();
Metody anonimowe i wyrażenia lambda
C# pozwala używać funkcji anonimowych i wyrażeń lambda bezpośrednio przy subskrypcji:
clicker.Clicked += () => Console.WriteLine("Kolejny klik!");
Albo trochę bardziej rozbudowanie, jeśli zdarzenie ma argumenty:
alarm.AlarmRaised += (sender, e) =>
{
Console.WriteLine($"Alarm wywołany przez: {sender}");
};
4. Przydatne niuanse
Subskrypcja i odsubskrypcja: ważne niuanse
Prawdziwe życie — to kiedy gości na imprezie robi się za dużo, albo ktoś chce wyjść do domu. Z delegatami jest tak samo: handler można dodać (subskrybować) i usunąć (odsubskrybować):
// Subskrypcja
publisher.MyEvent += MyHandler;
// Odsubskrypcja (gdy handler już nie jest potrzebny)
publisher.MyEvent -= MyHandler;
Dlaczego to ważne? Jeśli się nie odsubskrybujesz, zwłaszcza w dużych aplikacjach, handlery mogą zostać "wiszące" i nie pozwolić garbage collectorowi oczyścić obiektów — stąd wycieki pamięci.
Wiele handlerów
Na jedno zdarzenie można zasubskrybować dowolną liczbę handlerów. Zostaną one wywołane po kolei w tej kolejności, w jakiej zostały dodane.
clicker.Clicked += () => Console.WriteLine("Pierwszy handler!");
clicker.Clicked += () => Console.WriteLine("Drugi handler!");
clicker.Click();
// → Pierwszy handler!
// → Drugi handler!
Możesz nawet subskrybować i odsubskrybować handlery "w locie" — to elastyczne i wygodne.
Dlaczego to naprawdę ważne: realne scenariusze
- UI (Windows Forms, WPF, WinUI, MAUI): kliknięcie przycisku, zmiana tekstu w polu — to wszystko zdarzenia.
- FileSystemWatcher: powiadomienie o pojawieniu się nowych plików w folderze.
- Operacje asynchroniczne: zakończenie pobierania pliku, postęp wykonywania zadań.
- System pluginów: oddzielne moduły subskrybują zdarzenia głównej aplikacji.
Model zdarzeniowy pozwala budować rozszerzalne architektury: możesz dodawać nowe moduły i subskrybować istniejące zdarzenia bez zmiany bazowego kodu.
Przykłady subskrypcji
| Sygnatura zdarzenia | Przykład subskrypcji | Przykład handlera |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
5. Typowe błędy i pułapki
Błąd nr 1: niezgodność sygnatur handlera.
Jeśli zdarzenie jest zadeklarowane jako event Action<int>, a próbujesz zasubskrybować metodę bez parametrów, kompilator zgłosi błąd. Zawsze sprawdzaj, czy metoda odpowiada wymaganej sygnaturze zdarzenia.
Błąd nr 2: przechwytywanie zmiennych w lambdach.
Przy subskrypcji przez wyrażenie lambda może ono uchwycić zmienne z zewnętrznego kontekstu (patrz temat „Zamknięcia”). Jeśli po subskrypcji zmienna się zmieni, handler zobaczy już nową wartość, co może prowadzić do nieoczekiwanych rezultatów.
Błąd nr 3: subskrypcja na niezinicjalizowane zdarzenie.
Jeśli subskrybujesz zdarzenie zanim obiekt, który je zawiera, zostanie utworzony i zainicjalizowany, ryzykujesz NullReferenceException. Przed subskrypcją upewnij się, że obiekt jest gotowy do użycia.
Błąd nr 4: wielokrotna subskrypcja tego samego handlera.
Jeśli ten sam handler zostanie zasubskrybowany wielokrotnie, zostanie on wywołany tyle razy. To nie bug, a cecha działania zdarzeń, ale często bywa nieprzyjemnym zaskoczeniem.
GO TO FULL VERSION