CodeGym /Kurse /C# SELF /Typische Fehler mit Delegates und Events

Typische Fehler mit Delegates und Events

C# SELF
Level 54, Lektion 0
Verfügbar

1. Einleitung

Mit Delegates und Events in C# zu arbeiten ist angenehm und praktisch — die Sprache nimmt dir viel ab. Hinter diesem Vorhang verbergen sich jedoch eine Menge Fallstricke: unsichtbare Memory-Leaks, seltsame Bugs durch doppelte Subskriptionen, Desynchronisation von Handlern und sogar plötzliche Ausnahmen beim Benachrichtigen. Delegates und Events sind mächtig, erfordern aber sorgfältiges Management des Lebenszyklus von Objekten, ein Verständnis von Threads und Wissen darüber, wie Aufrufe und Abmeldungen genau funktionieren. Wenn deine Events „fast immer“ funktionieren, aber manchmal nicht feuern oder merkwürdige Fehler auslösen — du bist nicht allein! Schauen wir uns an, wo selbst erfahrene Entwickler am häufigsten danebenliegen und wie man das vermeidet.

Tabelle der häufigsten Fehler

Fehler Konsequenz Wie vermeiden
Doppelte Subskription Handler werden mehrfach aufgerufen Auf Subskription achten, vor dem Hinzufügen abmelden
Keine Abmeldung (Memory-Leak) Subscriber bleiben im Speicher hängen, „Zombies“ Immer abmelden, IDisposable verwenden
Ausnahme im Handler Die übrigen Handler werden nicht mehr aufgerufen try/catch in Handlern oder manuelles Iterieren
Modifikation der Subscriber während des Events Auslassen oder Duplizieren von Aufrufen Über eine Kopie der Handler iterieren (GetInvocationList())
Event-Aufruf wenn MyEvent == null NullReferenceException Auf null prüfen, ?.Invoke verwenden
Handler-Signatur passt nicht Kompilierfehler Signatur prüfen
Event von außen aufrufen Kompilierfehler Nur über OnEventName aufrufen
Static-Events an der falschen Stelle Vermischung von Subskriptionen Kein static ohne guten Grund
Closure-Probleme mit Lambdas Unerwartete Werte Variable kopieren

2. Mehrfache Subskription und multiple Aufrufe

Worum es geht

Wenn du denselben Handler mehrfach auf dasselbe Event setzt, fügt jeder +=-Aufruf deine Methode zur Delegate-Queue hinzu. Dadurch wird der Handler so oft aufgerufen, wie er hinzugefügt wurde.

Wie zeigt sich das?

Stell dir vor, du hast einen Button und einen Click-Handler:

Button btn = new Button();
btn.Click += OnButtonClick; // Wir abonnieren
btn.Click += OnButtonClick; // Und hier nochmal!

Jetzt wird bei jedem Klick die Methode OnButtonClick zweimal ausgeführt. Wenn du im Handler z.B. einen Zähler erhöhst oder ins Log schreibst, siehst du verdoppelte Ergebnisse.

Wie beheben?

Wiederholte Subskription entsteht oft durch fehlerhafte Struktur — z.B. wenn += in eine Methode ausgelagert ist, die mehrfach aufgerufen wird (in verschiedenen Lebenszyklusphasen einer Form).

  • Achte darauf, wo die Subskription passiert.
  • Setze += nicht in Phasen, die mehrfach ausgeführt werden können.
  • Manchmal ist eine „einzigartige“ Subskription nützlich — vor dem Hinzufügen den Handler entfernen:
myEvent -= MyHandler; // Zur Sicherheit entfernen
myEvent += MyHandler; // Dann wieder hinzufügen

Das ist sicher: wenn der Handler noch nicht da war, ändert -= nichts.

3. Der Zombie-Subscriber

Worum es geht

Wenn ein Subscriber ein Long-Lived Publisher-Event abonniert und sich nicht abmeldet, kann der GC das Subscriber-Objekt nicht sammeln: der Publisher hält noch eine Referenz auf den Delegate und damit auf das gesamte Subscriber-Objekt. Resultat — Memory-Leaks.

Typisches Beispiel

public class TemporaryPopup : IDisposable
{
    private Window _hostWindow;
    public TemporaryPopup(Window window)
    {
        _hostWindow = window;
        _hostWindow.Closed += OnHostClosed;
    }

    private void OnHostClosed(object sender, EventArgs e)
    {
        // ...
    }

    public void Dispose()
    {
        _hostWindow.Closed -= OnHostClosed; // Nicht vergessen abzumelden!
    }
}

Wenn du Dispose() vergisst oder nicht aufrufst, wird das Objekt nicht freigegeben — das Fenster hält noch die Referenz auf den Handler.

Wie vermeiden?

  • Implementiere IDisposable in Subscriber-Klassen, falls deren Lebenszeit kürzer ist als die des Publishers.
  • Nutze das using-Pattern oder rufe Dispose() explizit auf:
using (var popup = new TemporaryPopup(mainWindow))
{
    // ...
} // Hier wird Dispose automatisch aufgerufen

In GUI-Apps: meldet euch bei Closing/Dispose der Form/Window ab.

4. Ausnahmebehandlung in Handlern

Worum es geht

Wenn ein Event dutzende Handler hat und einer davon eine Ausnahme wirft, werden die restlichen Handler nicht mehr ausgeführt.

Demonstration

public event EventHandler MyEvent;

public void Raise()
{
    MyEvent?.Invoke(this, EventArgs.Empty);
}

Wenn einer der auf MyEvent registrierten Methoden eine Ausnahme wirft, bricht die Kette ab und die übrigen werden nicht mehr aufgerufen.

Wie damit umgehen?

  • Behandle Ausnahmen innerhalb der Handler lokal (try/catch) oder wirf sie bewusst weiter.
  • In komplexen Szenarien iteriere manuell über die Subscriber und isoliere Fehler jedes einzelnen:
var handlers = MyEvent?.GetInvocationList();
foreach (var handler in handlers)
{
    try
    {
        handler.DynamicInvoke(this, EventArgs.Empty);
    }
    catch (Exception ex)
    {
        // Logging, Failover, etc.
    }
}

5. Änderung der Subscriber-Liste während der Verteilung

Worum es geht

Wenn ein Handler sich selbst oder andere im Verlauf des Events abmeldet, kann das die Aufrufreihenfolge durcheinanderbringen: manche Handler werden übersprungen oder doppelt aufgerufen.

Wie vermeiden?

  • Ändere Subscriptions nicht aus Handlern heraus.
  • Wenn nötig — iteriere über eine Kopie der Delegates:
var handlers = MyEvent?.GetInvocationList();
foreach (EventHandler handler in handlers)
{
    handler(this, EventArgs.Empty);
}

6. Verwirrung um null-Delegate (keine Subscriber)

Worum es geht

Wenn niemand ein Event abonniert hat, ist der Delegate null. Ein Aufruf ohne Prüfung führt zu einer NullReferenceException.

Schlechtes Beispiel

public event EventHandler MyEvent;

public void Raise()
{
    MyEvent(this, EventArgs.Empty); // Wenn keine Subscriber — Exception!
}

Wie richtig?

  • Nutze den sicheren Aufruf: MyEvent?.Invoke(this, EventArgs.Empty).
  • Oder verwende das klassische thread-sichere Muster: kopiere den Delegate in eine lokale Variable und rufe diese auf.

7. Vermischung von Delegates/Events mit unterschiedlichen Signaturen

Worum es geht

Delegates sind strikt typisiert. Eine Signatur, die nicht mit der des Event-Delegates übereinstimmt, führt zu einem Kompilierfehler.

Beispiel

public event EventHandler<string> TextChanged;

void WrongHandler(object sender, int number) { /* ... */ }

TextChanged += WrongHandler; // Kompilierfehler!

Verwende die Standard-Delegates EventHandler und EventHandler<T> und achte auf exakte Signaturübereinstimmung.

8. Versuch, ein Event außerhalb des Publishers aufzurufen

Worum es geht

event ist ein gekapselter Delegate: externer Code kann ihn nicht direkt aufrufen (nur += / -= sind erlaubt).

Beispiel

public class MyPublisher
{
    public event EventHandler SomethingHappened;
}

var publisher = new MyPublisher();
publisher.SomethingHappened?.Invoke(publisher, EventArgs.Empty); // Fehler!

Wie richtig?

Der Aufruf sollte über eine geschützte/öffentliche Methode des Publishers erfolgen — üblicherweise OnEventName. Von außen nur +=/-=.

9. Fehler mit Accessors add und remove

Worum es geht

Custom Accessors für Events erlauben Kontrolle über Subskriptionen, aber leicht kann man damit die korrekte Aufrufreihenfolge oder Thread-Safety zerstören.

Beispiel

public event EventHandler MyEvent
{
    add { /* ... */ }
    remove { /* ... */ }
}

Wenn du dir nicht sicher bist — nutze die Standard-Event-Implementierung. Bei manueller Implementierung: Dokumentation lesen und Synchronisation beachten.

10. Zugriff auf Events aus statischen und nicht-statischen Kontexten

Worum es geht

Es ist leicht, versehentlich ein Event static zu deklarieren, obwohl es instanzgebunden sein sollte. Dann teilen alle Objekte die gleiche Subscriber-Liste.

Beispiel

public static event EventHandler GlobalEvent; // Oops!
// Instanzen verlieren ihre Individualität, Subskriptionen vermischen sich global

Wie vermeiden?

Mache ein Event nur dann static, wenn wirklich ein globaler Kontext gewünscht ist (z.B. globales Logging). In den meisten Fällen: Instanzgekapselte Events.

11. Probleme mit Variable-Capture in Lambda-Ausdrücken

Worum es geht

Lambda-Ausdrücke capturen Variablen per Referenz. In Schleifen führt das oft zum „letzten Wert“-Problem.

Beispiel

for (int i = 0; i < 5; i++)
{
    button.Click += (s, e) => Console.WriteLine(i);
}
// Alle Handler werden "5" ausgeben

Wie richtig?

for (int i = 0; i < 5; i++)
{
    int copy = i; // Lokale Kopie
    button.Click += (s, e) => Console.WriteLine(copy);
}

12. Mischung aus schwachen und starken Referenzen: Advanced "Weak Events"

In großen Anwendungen (z.B. WPF) verwendet man das Konzept der „Weak Events“, bei dem der Publisher eine schwache Referenz zum Subscriber hält, damit der GC den Subscriber nicht blockiert. Weak Events helfen gegen Leaks, aber der Subscriber kann gesammelt werden und dann das Event verpassen.

Mehr Details: Weak Event Patterns (MSDN)

13. Fehlender Standard für Namensgebung und Signaturen von Events

Worum es geht

Halte dich an Standard-Signaturen und -Namen: Events im Perfekt/Partizip („Changed“, „Closed“, „Completed“), Argumente sollten von EventArgs ableiten.

„Falsch“-Beispiel:

public delegate void SomethingHappens(int what);
// ...
public event SomethingHappens Something;

„Richtig“-Beispiel:

public event EventHandler<EventArgs> SomethingHappened;

Für eigene Events benutzt fast immer EventHandler oder EventHandler<T>. Deine Kollegen (und dein zukünftiges Ich) werden es dir danken.

2
Aufgabe
C# SELF, Level 54, Lektion 0
Gesperrt
Exception-Handling in Event-Handlern
Exception-Handling in Event-Handlern
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION