CodeGym /Kursy /C# SELF /Szczegółowa analiza subskrypcji (

Szczegółowa analiza subskrypcji ( +=)

C# SELF
Poziom 53 , Lekcja 0
Dostępny

1. Wprowadzenie

Zdarzenia w C# — to nie są po prostu zmienne, do których można przypisać delegat. To chroniona lista handlerów, i tylko właściciel zdarzenia może uruchomić jej wykonanie; inni mogą jedynie dodawać (+=) lub usuwać (-=) swoje reakcje.

Na przykład, tak można się zasubskrybować na zdarzenie:

worker.WorkCompleted += Worker_WorkCompleted;

Na pierwszy rzut oka wygląda to jak zwykłe dodawanie, ale w praktyce działa inaczej. Pod maską zdarzenie przechowuje łańcuch wywołań (invocation list) — zestaw delegatów, które trzeba wywołać przy wystąpieniu zdarzenia. Kiedy piszesz +=, do tej listy dodawany jest nowy handler.

Rozłóżmy to szczegółowo, co się dzieje wewnątrz, jak formuje się ten łańcuch i jakie niuanse mogą się pojawić przy subskrypcji.

Co to jest łańcuch-delegat?

Przypomnijmy, delegaty w C# są „multicastowe”: można im przypisać kilka metod i wszystkie zostaną wykonane po kolei, jeśli delegat zostanie wywołany. Zdarzenia korzystają z tego mechanizmu: ich wartość to w zasadzie delegat z listą handlerów.

W kategoriach kodu:

public event EventHandler<WorkCompletedEventArgs> WorkCompleted;

Kiedy ktoś się subskrybuje:

worker.WorkCompleted += MyHandler;

C# pod maską robi mniej więcej tak:

  • Bierze aktualny delegat (listę handlerów).
  • Wywołuje na nim metodę Delegate.Combine (łączy handlery).
  • Zapisuje zaktualizowany łańcuch z powrotem do zmiennej zdarzenia.

Schematycznie:

Operacja Wewnętrzna lista handlerów zdarzenia
Przed null lub [Handler1]
Po += Handler2 [Handler1, Handler2]
Po jeszcze += H3 [Handler1, Handler2, Handler3]

Ciekawostka: mechanizm multicast-delegatów to nie „magia”, tylko konkretna implementacja: delegat pod maską zawiera tablicę metod do wywołania.

2. Jak działa subskrypcja: wyjaśnienie na chłopski rozum

Rozważmy to na konkretnym przykładzie. Niech mamy wydawcę (Worker) i subskrybentów (Logger, Notifier):

public class Worker
{
    public event EventHandler<WorkCompletedEventArgs> WorkCompleted;

    public void DoWork()
    {
        // ... praca ...
        OnWorkCompleted("Zadanie zakończone!");
    }

    protected virtual void OnWorkCompleted(string message)
    {
        WorkCompleted?.Invoke(this, new WorkCompletedEventArgs { Message = message });
    }
}

public class Logger
{
    public void LogWorkCompleted(object? sender, WorkCompletedEventArgs e)
    {
        Console.WriteLine("Log: " + e.Message);
    }
}

public class Notifier
{
    public void ShowNotification(object? sender, WorkCompletedEventArgs e)
    {
        Console.WriteLine("Powiadomienie: " + e.Message);
    }
}

W Main:

var worker = new Worker();
var logger = new Logger();
var notifier = new Notifier();

worker.WorkCompleted += logger.LogWorkCompleted;
worker.WorkCompleted += notifier.ShowNotification;

// Uruchomienie pracy
worker.DoWork();

Kiedy wywoływane jest OnWorkCompleted, zdarzenie najpierw wywoła logger.LogWorkCompleted, potem notifier.ShowNotification (w tej kolejności, w jakiej się zasubskrybowano).

3. Przydatne niuanse

Wizualizacja: jak zdarzenie przechowuje handlery

+---------------------+
| Worker              |
|---------------------|
| WorkCompleted Event |    ---> [ LogWorkCompleted, ShowNotification ]
+---------------------+

Przy subskrypcji każdy nowy handler "dokłada się" do istniejącej listy. Po wywołaniu zdarzenia delegat wywołuje po kolei wszystkie podpisane metody.

Wielokrotna subskrypcja tą samą metodą

worker.WorkCompleted += logger.LogWorkCompleted;
worker.WorkCompleted += logger.LogWorkCompleted; // Dwa razy!

W tym przypadku handler zostanie wywołany tyle razy, ile razy jest zasubskrybowany — tu dwa razy z rzędu.

Subskrypcja lambdą

worker.WorkCompleted += (sender, e) => Console.WriteLine("Anonimowy handler: " + e.Message);

Jeśli taką lambdę zasubskrybujesz wielokrotnie — analogicznie, będzie wywoływana tyle razy przy każdym zdarzeniu. Jednak uwaga: każda lambda tworzy własny obiekt-delegat, więc nie da się jej tak po prostu odsubskrybować, jeśli nie przechowasz referencji (więcej w wykładzie 260 i dalej).

Jak działa subskrypcja wewnątrz: analiza niskopoziomowa

Zdarzenie — to specjalne property z dwoma akcesorami (add/remove), które są wywoływane przy użyciu += i -=. Upraszczając, kompilator generuje mniej więcej taki kod:

// Mniej więcej tak (uproszczone)
public event EventHandler<WorkCompletedEventArgs> WorkCompleted
{
    add { /* kod dodania handlera */ }
    remove { /* kod usunięcia handlera */ }
}

Domyślnie używana jest standardowa implementacja: delegat jest łączony przy pomocy Delegate.Combine i usuwany przez Delegate.Remove.

To chroni zdarzenie: z zewnątrz nie można wywołać zdarzenia bezpośrednio (żadnego worker.WorkCompleted(...);), można tylko subskrybować lub odsubskrybować.

Mechanika subskrypcji: rysujemy schemat

             +----------------------+
             |                      |
             v                      v
    +--------------------+   +----------------------+
    | LogWorkCompleted   |   | ShowNotification    |
    +--------------------+   +----------------------+
             ^                      ^
             \______________________/
                       ^
                       |
               WorkCompleted Event

Tak zwany "Invocation List" — łańcuch wywołań.

Po co to ważne: praktyczne znaczenie

Zrozumienie mechaniki subskrypcji to klucz do zarządzania powiązaniami między obiektami. Możesz budować skomplikowane systemy, gdzie komponenty dynamicznie się subskrybują i odsubskrybują od zdarzeń, bez tworzenia twardych zależności. To standard w UI-frameworkach, silnikach gier, aplikacjach serwerowych i nawet w nowoczesnych architekturach microservices (tam to już na poziomie kolejek, ale idea ta sama).

Na rozmowach kwalifikacyjnych często pytają, jak działa model zdarzeń w C#, dlaczego zdarzenia czynią system elastycznym i jak poprawnie zarządzać cyklem życia subskrypcji.

4. Co można (i czego nie można) robić z zewnątrz klasy

Można

  • Subskrybować zdarzenie (+=)
  • Odsubskrybować (-=)

Nie można

  • Wywołać zdarzenia bezpośrednio
  • Przypisać zdarzeniu delegata bezpośrednio (worker.WorkCompleted = ... — błąd!)

Te ograniczenia realizuje słowo kluczowe event. Gdybyś zadeklarował delegat jako zwykłe pole:

public EventHandler<WorkCompletedEventArgs> WorkCompleted; // nie event!

— każdy mógłby robić wszystko, włącznie z wyczyszczeniem handlerów, co wprowadziłoby bałagan i potencjalne bugi. Dlatego niemal zawsze używaj tylko event!

Czy można subskrybować jedną metodą kilka zdarzeń?

Tak! To się nazywa "multisubscribe". Na przykład:

worker.WorkCompleted += logger.LogWorkCompleted;
anotherWorker.WorkCompleted += logger.LogWorkCompleted;

Jeśli podpisałeś tę samą metodę na zdarzenia różnych obiektów, handler zadziała i dla jednego, i dla drugiego zdarzenia. W handlerze możesz rozpoznać, kto wywołał zdarzenie, po parametrze sender.

Prawdziwy przypadek: dynamiczne subskrypcje

Wyobraź sobie aplikację, w której użytkownik może uruchamiać kilka zadań równolegle. Dla każdego nowego zadania tworzony jest obiekt Worker, na którego zdarzenie subskrybuje się ten sam handler — np. metoda loggera logger.LogWorkCompleted.

var allWorkers = new List<Worker>();
for (int i = 0; i < 10; i++)
{
    var w = new Worker();
    w.WorkCompleted += logger.LogWorkCompleted;
    allWorkers.Add(w);
}

W efekcie, gdy któreś z zadań się zakończy, logger dostanie powiadomienie i zapisze, co i kiedy się wydarzyło.

Praktyczne uwagi: jak zobaczyć subskrybentów zdarzenia

W zwykłym kodzie nie da się bezpośrednio dowiedzieć, ile handlerów jest zasubskrybowanych na zdarzenie (zdarzenie enkapsuluje delegat). Jednak wewnątrz klasy-wydawcy możesz pracować z delegatem zdarzenia bezpośrednio, np. sprawdzić jego GetInvocationList():

// Tylko wewnątrz klasy-wydawcy
var handlers = WorkCompleted?.GetInvocationList();
if (handlers != null)
    Console.WriteLine($"Subskrybentów: {handlers.Length}");

To przydaje się, jeśli chcesz zrobić niestandardową logikę rozsyłania lub debugowania (choć lepiej używać tego tylko do celów edukacyjnych!).

5. Typowe błędy i cechy

Handler nie został dodany? Nie zostanie wywołany!
Jeśli się nie zasubskrybujesz, handler nigdy nie zostanie wywołany. Przed wywołaniem zdarzenia warto sprawdzić null (inaczej dostaniesz NullReferenceException).

Wielokrotna subskrypcja
Jeśli subskrybujesz wielokrotnie tę samą metodę na jednym zdarzeniu — handler wykona się tyle razy. Czasem to pułapka: przypadkowe wielokrotne wywołanie += powoduje, że jedno powiadomienie zamienia się w kilka identycznych.

Metody statyczne/instancyjne
Możesz subskrybować zarówno metody statyczne, jak i instancyjne. Najważniejsze to prawidłowa sygnatura.

public static void StaticHandler(object? sender, WorkCompletedEventArgs e) { /* ... */ }
worker.WorkCompleted += StaticHandler;

Lambdy i subskrypcje w pętli
Jeśli np. w pętli subskrybujesz lambdy, upewnij się, że rozumiesz, co się dzieje z zmiennymi, które lambda "złapała" (captured). Można przypadkowo złapać nie to, co się oczekuje.

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