CodeGym /Kurse /C# SELF /Abonnenten und sicheres Auslösen von Events

Abonnenten und sicheres Auslösen von Events

C# SELF
Level 53 , Lektion 2
Verfügbar

1. Einführung

Wenn wir schreiben

worker.WorkCompleted += listener.OnWorkCompleted;

fügen wir eigentlich einen Zeiger auf eine Methode zur "Invocation-List" (multicast delegate) des Events hinzu. Diese "Liste" im Event ist einfach eine Sequenz von Methoden, die beim Auslösen des Events aufgerufen werden. In C# ist ein Event auf einem Delegate aufgebaut, das mehrere Subscriber unterstützen kann.

Stell dir einen Newsletter vor: du hast eine Liste von Abonnenten (E-Mail-Adressen). Wenn du den Newsletter verschickst (das Event auslöst), bekommen alle Abonnenten die Mail. Wenn sich jemand abmeldet, wird er aus der Liste entfernt und bekommt keine Mails mehr.

Wie man einen Subscriber hinzufügt oder entfernt

Subscription (+=) und Unsubscription (-=) arbeiten mit dem Delegate hinter dem Event. Hier ein Beispiel mit einer Lambda, die man sowohl subscriben als auch unsubscriben kann:

EventHandler<WorkCompletedEventArgs> handler = (sender, e) =>
{
    Console.WriteLine($"[Lambda] Arbeit abgeschlossen: {e.Message}");
};

worker.WorkCompleted += handler; // Abonnieren
worker.WorkCompleted -= handler; // Abmelden

Bei normalen Methoden sieht die Abmeldung genauso aus:

worker.WorkCompleted += listener.OnWorkCompleted;
worker.WorkCompleted -= listener.OnWorkCompleted;

Beachte: wenn du dieselbe Methode mehrmals abonnierst, wird sie auch so oft aufgerufen, und zum Abmelden musst du -= genauso oft aufrufen.

2. Warum überhaupt manuell verwalten?

Warum ist es wichtig, Subscriber zu verwalten?

In realen Anwendungen, besonders langlebigen (z.B. Desktop- oder Server-Apps), kann falsches Management von Subscriptions zu Memory Leaks führen. Wenn ein Subscriber nicht mehr gebraucht wird, aber noch in der Subscriber-Liste des Events hängt, wird er vom Garbage Collector nicht eingesammelt — weil noch eine Referenz vom Event-Delegate auf ihn zeigt.

Illustration

Aktion Ergebnis für den Subscriber
+= (abonniert) Zur Liste hinzugefügt
-= (abgemeldet) Aus der Liste entfernt
Subscriber-Objekt gelöscht Wenn NICHT abgemeldet! — wird NICHT gelöscht, da noch eine Referenz im Event existiert
Subscriber-Objekt gelöscht Wenn ABGEMELDET — wird normal freigegeben

Wie findet man heraus, wer auf ein Event subscribed ist?

Events kapseln die Subscriber-Liste, daher kann man außerhalb der Publisher-Klasse diese Liste nicht direkt einsehen — man kann nur Handler hinzufügen (+=) oder entfernen (-=).

Innerhalb der Klasse, in der das Event auf Basis eines Delegates (z.B. EventHandler) deklariert ist, kann man jedoch die aktuelle Subscriber-Liste über GetInvocationList() bekommen:

// Innerhalb der Publisher-Klasse
if (WorkCompleted != null)
{
    foreach (Delegate subscriber in WorkCompleted.GetInvocationList())
    {
        Console.WriteLine($"Handler: {subscriber.Method.Name}, Objekt: {subscriber.Target}");
    }
}

Das ist im Alltag selten nötig, kann aber beim Debugging oder zur Implementierung einer Massen-Abmeldung aller Subscriber nützlich sein.

3. Sicheres Auslösen von Events: "Minensuche" und wie man sie umgeht

Was kann beim Auslösen eines Events schiefgehen?

Alles sieht einfach aus: man ruft

WorkCompleted?.Invoke(this, args);

und meistens klappt alles. Aber es gibt Nuancen. Hier sind sie:

1. Multithread-Gefahr

In Multithread-Anwendungen kann es passieren, dass zwischen der Null-Prüfung des Events und dem Aufruf die Subscriptions von einem anderen Thread geändert werden. Zum Beispiel:

1) Thread A prüft: WorkCompleted != null.
2) Zur gleichen Zeit meldet sich Thread B vom Event ab (-=) und die Handler-Liste wird leer.
3) Thread A versucht, WorkCompleted.Invoke(...) aufzurufen — es kommt zur NullReferenceException, weil keine Handler mehr da sind.

Das ist eine klassische Race-Condition beim Arbeiten mit Events.

2. Unerwartete Ausnahmen in Handlern

Wenn einer der Subscriber beim Verarbeiten des Events eine Ausnahme wirft, werden die Aufrufe der restlichen Handler abgebrochen. Das Event "bricht" beim ersten Fehler und die anderen Subscriber bekommen keine Benachrichtigung mehr. Um das zu vermeiden, sollte man den Aufruf jedes einzelnen Handlers in ein try-catch packen, falls alle benachrichtigt werden müssen.

3. Ungewollte Kontext-Leaks

Ein Event-Handler ist oft eine Instanzmethode, die eine Referenz auf das Subscriber-Objekt (this) hält. Wenn sich der Subscriber vergisst, vom Publisher abzumelden, bleibt die Referenz in der Delegate-Liste des Publishers bestehen. Dadurch kann der Garbage Collector das Subscriber-Objekt nicht freigeben — Memory Leak.

Wie ruft man ein Event sicher auf?

1) Delegate in eine lokale Variable kopieren

Aufruf über eine lokale Variable stellt sicher, dass die Subscriber-Liste während des Aufrufs nicht verändert wird:

// Der alte, bewährte Weg
var handler = WorkCompleted;
if (handler != null)
{
    handler(this, args);
}

Oder moderner mit dem null-conditional Operator:

WorkCompleted?.Invoke(this, args);

In den meisten Fällen reicht das, weil der C#-Compiler diese Konstruktion intern absichert (siehe offizielle Dokumentation).

2) Schutz vor Ausnahmen der Handler

Wenn es kritisch ist, dass wirklich alle Handler aufgerufen werden (auch wenn einer fällt), iteriere man manuell:

var handler = WorkCompleted;
if (handler != null)
{
    foreach (EventHandler<WorkCompletedEventArgs> subscriber in handler.GetInvocationList())
    {
        try
        {
            subscriber(this, args);
        }
        catch (Exception ex)
        {
            // Loggen, aber nicht das ganze Event abstürzen lassen
            Console.WriteLine($"Fehler im Handler: {ex.Message}");
        }
    }
}

So ein Ansatz ist im normalen UI-Code selten nötig, aber hilfreich in Libraries, Loggern oder komplexen Systemen.

3) Memory Leaks verhindern

Wenn ein Subscriber kürzer lebt als der Publisher (z.B. ein Fenster, das sich auf ein App-Event subscribiert), muss er sich abmelden:

worker.WorkCompleted -= listener.OnWorkCompleted;

Andernfalls kann der garbage collector das listener-Objekt nicht freigeben, selbst wenn es keine "offensichtlichen" Referenzen mehr gibt.

4. Praktisches Beispiel: Manager für Massen-Subscription und -Unsubscription

Erweitern wir unsere Lernapp. Angenommen, wir haben mehrere Listener und wollen sie dynamisch während der Programmlaufzeit an- und abmelden.

public class WorkListener
{
    private readonly string _name;

    public WorkListener(string name)
    {
        _name = name;
    }

    public void OnWorkCompleted(object sender, WorkCompletedEventArgs e)
    {
        Console.WriteLine($"Listener {_name}: {e.Message}");
    }
}

In der Main-Anwendung:

var worker = new Worker();

var listeners = new List<WorkListener>
{
    new WorkListener("Ivan"),
    new WorkListener("Maria"),
    new WorkListener("Denis")
};

// Alle Listener abonnieren
foreach (var listener in listeners)
    worker.WorkCompleted += listener.OnWorkCompleted;

// Event auslösen
worker.DoWork();

// Massen-Abmeldung
foreach (var listener in listeners)
    worker.WorkCompleted -= listener.OnWorkCompleted;

// Prüfen, dass niemand mehr reagiert
worker.DoWork();

In der Konsole erscheinen nach dem ersten DoWork 3 Nachrichten, nach dem zweiten — keine.

5. Tipps für sicheres Arbeiten mit Events

  • Melde dich rechtzeitig ab, wenn der Lebenszyklus des Subscribers kürzer ist als der des Publishers.
  • Wenn du das Muster "langlebiger Publisher — temporärer Subscriber" implementierst, sorge immer für Abmeldung, z.B. in Dispose(), beim Schließen eines Fensters oder bei anderem expliziten Lebensende des Objekts.
  • Für einmalige Events kannst du einen anonymen Lambda-Handler benutzen und dich direkt darin wieder abmelden:
EventHandler<WorkCompletedEventArgs> handler = null;
handler = (s, e) => 
{
    Console.WriteLine("Event einmalig verarbeitet!");
    worker.WorkCompleted -= handler;
};
worker.WorkCompleted += handler;
  • Speichere keine Referenzen auf Subscriber oder Handler nur, um zu prüfen, "wer subscribed" — das ist in der normalen Business-Logik unnötig. Mach das nur zum Debugging.

6. Häufige Fehler und wie man sie vermeidet

Fehler Nr.1: nicht abgemeldet — Memory Leak.
Wenn ein Subscriber sich nicht abmeldet, besonders in großen Anwendungen mit vielen Events und Subscribern, können Objekte länger im Speicher bleiben als nötig. Solch ein Fehler tritt oft erst spät sichtbar auf, führt aber zu steigendem Speicherverbrauch und schlechterer Performance.

Fehler Nr.2: Event ohne Null-Check aufrufen.
Wenn ein Event keine Subscriber hat und man versucht, es direkt aufzurufen, entsteht eine NullReferenceException. In neueren C#-Versionen hilft der null-conditional Operator ?., aber bei älterem Code oder bei manueller Iteration über Handler darf man den Null-Check nicht vergessen.

Fehler Nr.3: Ausnahme in einem Handler bricht die restlichen Aufrufe ab.
Wenn einer der Handler eine Ausnahme wirft, werden die folgenden Handler nicht mehr aufgerufen. Wenn es wichtig ist, dass alle Subscriber benachrichtigt werden, iteriere über die Handler und schütze jeden Aufruf mit try/catch.

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