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.
GO TO FULL VERSION