CodeGym /Kursy /C# SELF /Subskrypcja zdarzenia i tworzenie handlerów

Subskrypcja zdarzenia i tworzenie handlerów

C# SELF
Poziom 52 , Lekcja 2
Dostępny

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
event Action
ev += Handler;
void Handler() { ... }
event Action<int>
ev += (x) => { ... };
void Handler(int x) { ... }
event EventHandler
ev += Handler;
void Handler(object s, EventArgs e) { ... }
event EventHandler<CustomArgs>
ev += Handler;
void Handler(object s, CustomArgs e) { ... }

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.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION