1. Einführung
Events in C# sind nicht einfach Variablen, in die man einen Delegate schreibt. Das ist eine geschützte Liste von Handlern, und nur der Besitzer des Events kann dessen Ausführung auslösen; alle anderen können nur hinzufügen (+=) oder entfernen (-=) ihre Reaktionen.
Zum Beispiel kann man sich so auf ein Event subscriben:
worker.WorkCompleted += Worker_WorkCompleted;
Auf den ersten Blick sieht das aus wie normale Addition, aber es funktioniert anders. Unter der Haube speichert das Event eine Aufrufkette (invocation list) — eine Menge von Delegates, die beim Auslösen des Events aufgerufen werden müssen. Wenn du += schreibst, wird ein neuer Handler zu dieser Liste hinzugefügt.
Lass uns genauer anschauen, was dabei intern passiert, wie die Kette gebildet wird und welche Feinheiten bei der Subscription auftreten können.
Was ist eine Delegate-Kette?
Erinnerung: Delegates in C# sind "multicast": man kann mehrere Methoden zuweisen, und alle werden nacheinander ausgeführt, wenn der Delegate aufgerufen wird. Events nutzen diesen Mechanismus: ihr Wert ist im Grunde ein Delegate mit einer Liste von Handlern.
In Code-Begriffen:
public event EventHandler<WorkCompletedEventArgs> WorkCompleted;
Wenn sich jemand subscribet:
worker.WorkCompleted += MyHandler;
macht C# unter der Haube ungefähr so:
- Holt den aktuellen Delegate (die Liste der Handler).
- Ruft Delegate.Combine auf (verbindet Handler).
- Schreibt die aktualisierte Kette zurück in die Event-Variable.
Schematisch:
| Operation | Interne Liste der Event-Handler |
|---|---|
| Vorher | null oder [Handler1] |
| Nach += Handler2 | [Handler1, Handler2] |
| Nach noch einem += H3 | [Handler1, Handler2, Handler3] |
Interessanter Fakt: der Mechanismus der multicast-Delegates ist keine "Magie", sondern eine konkrete Implementierung: ein Delegate enthält intern ein Array von Methoden, die aufgerufen werden sollen.
2. Wie Subscription funktioniert: einfache Erklärung
Schauen wir uns ein konkretes Beispiel an. Angenommen, wir haben einen Publisher (Worker) und Subscriber (Logger, Notifier):
public class Worker
{
public event EventHandler<WorkCompletedEventArgs> WorkCompleted;
public void DoWork()
{
// ... arbeit ...
OnWorkCompleted("Task completed!");
}
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("Notification: " + e.Message);
}
}
In Main:
var worker = new Worker();
var logger = new Logger();
var notifier = new Notifier();
worker.WorkCompleted += logger.LogWorkCompleted;
worker.WorkCompleted += notifier.ShowNotification;
// Start der Arbeit
worker.DoWork();
Wenn OnWorkCompleted aufgerufen wird, ruft das Event zuerst logger.LogWorkCompleted auf, dann notifier.ShowNotification (in genau der Reihenfolge, in der du dich subscribet hast).
3. Nützliche Feinheiten
Visualisierung: wie das Event Handler speichert
+---------------------+
| Worker |
|---------------------|
| WorkCompleted Event | ---> [ LogWorkCompleted, ShowNotification ]
+---------------------+
Bei Subscription "hängt" sich jeder neue Handler an die vorhandene Liste. Nach dem Auslösen ruft der Delegate nacheinander alle subscribe-ten Methoden auf.
Mehrfache Subscription desselben Methods
worker.WorkCompleted += logger.LogWorkCompleted;
worker.WorkCompleted += logger.LogWorkCompleted; // Zweimal!
In diesem Fall wird der Handler so oft aufgerufen, wie er subscribet wurde — hier also zweimal hintereinander.
Subscription mit Lambda
worker.WorkCompleted += (sender, e) => Console.WriteLine("Anonymer Handler: " + e.Message);
Wenn du dieselbe Lambda mehrfach subscribest — wird sie entsprechend mehrfach aufgerufen. Aber Achtung: jede Lambda erzeugt ihr eigenes Delegate-Objekt, deshalb kannst du nicht so einfach wieder unsubscriben (mehr dazu in Vorlesung 260 und danach).
Wie Subscription intern funktioniert: Low-Level-Analyse
Ein Event ist eine spezielle Property mit zwei Accessoren (add/remove), die bei Verwendung von += und -= aufgerufen werden. Vereinfacht generiert der Compiler etwa so einen Code:
// Ungefähr so (vereinfacht)
public event EventHandler<WorkCompletedEventArgs> WorkCompleted
{
add { /* code zum Hinzufügen des Handlers */ }
remove { /* code zum Entfernen des Handlers */ }
}
Standardmäßig wird der Delegate mit Delegate.Combine kombiniert und mit Delegate.Remove entfernt.
Das schützt das Event: von außen kann man das Event nicht direkt aufrufen (kein worker.WorkCompleted(...);), man kann nur subscriben oder unsubscriben.
Mekanik der Subscription: Diagramm
+----------------------+
| |
v v
+--------------------+ +----------------------+
| LogWorkCompleted | | ShowNotification |
+--------------------+ +----------------------+
^ ^
\______________________/
^
|
WorkCompleted Event
Das ist die sogenannte "Invocation List" — die Aufrufkette.
Warum das wichtig ist: praktischer Nutzen
Das Verständnis der Subscription-Mechanik ist der Schlüssel zur Steuerung von Beziehungen zwischen Objekten. So kannst du komplexe Systeme bauen, in denen Komponenten dynamisch subscriben und unsubscriben, ohne enge Kopplung. Das ist Standard in UI-Frameworks, Game Engines, Server-Anwendungen und sogar in modernen Microservice-Architekturen (dort meist auf Ebene von Queues, aber die Idee bleibt ähnlich).
Bei Bewerbungsgesprächen wird oft gefragt, wie das Event-Modell in C# funktioniert, warum Events Systeme flexibel machen und wie man den Lebenszyklus von Subscriptions korrekt verwaltet.
4. Was man von außen an der Klasse machen kann (und was nicht)
Erlaubt
- Sich auf ein Event subscriben (+=)
- Unsubscriben (-=)
Nicht erlaubt
- Das Event direkt aufrufen
- Dem Event direkt einen Delegate zuweisen (worker.WorkCompleted = ... — Fehler!)
Diese Einschränkungen werden durch das Schlüsselwort event durchgesetzt. Wenn du den Delegate als normales Feld deklarieren würdest:
public EventHandler<WorkCompletedEventArgs> WorkCompleted; // nicht event!
— dann könnte jeder alles tun, inklusive Handler zu nullen, was Chaos und Bugs verursachen würde. Deshalb fast immer: nur event verwenden!
Kann man dieselbe Methode auf mehrere Events subscriben?
Ja! Das nennt sich "multisubscribing". Beispiel:
worker.WorkCompleted += logger.LogWorkCompleted;
anotherWorker.WorkCompleted += logger.LogWorkCompleted;
Wenn du dieselbe Methode auf Events verschiedener Objekte subscribest, wird der Handler bei beiden Events aufgerufen. Innerhalb des Handlers kannst du anhand des sender-Parameters erkennen, wer das Event ausgelöst hat.
Praktischer Use-Case: dynamische Subscriptions
Stell dir vor, deine App startet mehrere Aufgaben parallel. Für jede neue Aufgabe wird ein Worker-Objekt erstellt, und derselbe Handler wird auf dessen Event subscribet — zum Beispiel 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);
}
Folglich bekommt der Logger beim Abschluss jeder Aufgabe eine Benachrichtigung und protokolliert, was wann passiert ist.
Praxis-Tipp: wie man Subscriber eines Events sehen kann
Im normalen Code kann man nicht direkt herausfinden, wie viele Handler auf ein Event subscribet sind (das Event kapselt den Delegate). Innerhalb der Publisher-Klasse kannst du aber mit dem Delegate arbeiten, z.B. GetInvocationList() nutzen:
// Nur innerhalb der Publisher-Klasse
var handlers = WorkCompleted?.GetInvocationList();
if (handlers != null)
Console.WriteLine($"Subscribers: {handlers.Length}");
Das ist nützlich für spezielle Dispatch-Logik oder Debugging (aber am besten nur zu Lernzwecken einsetzen!).
5. Typische Fehler und Besonderheiten
Handler nicht hinzugefügt? Dann wird er nie aufgerufen!
Wenn du nicht subscribet hast, wird der Handler niemals ausgeführt. Vor dem Aufruf eines Events sollte man immer auf null prüfen (ansonsten droht eine NullReferenceException).
Mehrfaches Subscriben
Wenn du denselben Handler mehrfach auf dasselbe Event abonnierst — wird er entsprechend oft aufgerufen. Das ist eine häufige Falle: ein versehentlicher doppelter += kann eine einzelne Benachrichtigung in mehrere identische verwandeln.
Statische vs. Instanz-Methoden
Du kannst sowohl statische als auch Instanzmethoden subscriben. Wichtig ist nur die korrekte Signatur.
public static void StaticHandler(object? sender, WorkCompletedEventArgs e) { /* ... */ }
worker.WorkCompleted += StaticHandler;
Lambdas und Subscribing in Schleifen
Wenn du in einer Schleife Lambdas subscribest, achte darauf, welche Variablen die Lambda captured. Du kannst leicht das falsche Value einfangen, wenn du die Closure nicht richtig verstehst.
GO TO FULL VERSION