1. Einführung
In gewisser Weise ist das Abonnieren eines Events in C# wie das Abonnieren eines Meme-Newsletters von einem Freund: die Nachrichten kommen, bis du "genug" sagst und dich abmeldest. In der Programmierung ist das besonders wichtig, weil eine vergessene Subscription nicht einfach "noch ein Meme" ist, sondern ein Memory Leak!
Stell dir vor, du hast ein Formular in der Anwendung (z. B. ein zusätzliches Einstellungsfenster). Es subscribt sich auf ein Event des Hauptfensters, um auf Änderungen zu reagieren. Der Nutzer schließt das Formular und denkt, es sei zerstört, aber der Handler ist noch angemeldet! Das Formular lebt immer noch im Speicher, weil das Hauptobjekt über das Event eine Referenz auf es hält.
Fazit: Wenn ein Subscriber sich beim Publisher angemeldet hat und "vergisst", sich abzumelden, wird der Garbage Collector das Objekt nicht aus dem Speicher entfernen, solange der Publisher lebt.
Wiederholen wir den Operator += und zeigen -=
- += — Subscription: fügt den Handler zur Invocation-Liste des Events hinzu.
- -= — Abmeldung: entfernt den Handler aus der Invocation-Liste.
Das sieht ungefähr so aus:
worker.WorkCompleted += handler; // subscription
worker.WorkCompleted -= handler; // unsubscription
Wenn ein Handler zweimal hinzugefügt wurde, muss er genauso oft entfernt werden, damit er wirklich aus der Liste verschwindet.
Ein bisschen internals
Hinter den Kulissen ist ein Event in C# ein Feld-Delegate (oder eine Liste von Delegates), und der Operator += ruft faktisch Delegate.Combine auf, während -= Delegate.Remove aufruft. Das Objekt, das sich angemeldet hat, wird Teil des Referenzgraphen. Deshalb bedeutet eine vergessene Subscription = Memory Leak.
2. Memory Leaks durch Events: wie das funktioniert
Klassische Situation
class Window
{
public event EventHandler Updated;
public void SimulateUpdate()
{
// Simulation: benachrichtigt alle Subscriber
Updated?.Invoke(this, EventArgs.Empty);
}
}
class SettingsForm
{
public void OnWindowUpdated(object sender, EventArgs e)
{
Console.WriteLine("SettingsForm reagiert auf Fenster-Update");
}
}
Schritt für Schritt:
var window = new Window();
var settingsForm = new SettingsForm();
window.Updated += settingsForm.OnWindowUpdated;
window.SimulateUpdate(); // SettingsForm reagiert
// Der Nutzer schließt das Formular. Wir verlieren alle Referenzen darauf:
settingsForm = null;
// Aber das SettingsForm-Objekt wird NICHT vom GC gelöscht, solange window lebt,
// weil window.Updated immer noch eine Referenz auf die Methode OnWindowUpdated hält,
// und damit auf das SettingsForm-Objekt selbst.
Was tun?
Abmelden:
// Dafür brauchen wir eine Referenz auf den Handler oder das Objekt:
window.Updated -= settingsForm.OnWindowUpdated;
settingsForm = null; // Jetzt kann das Objekt gesammelt werden
Tabelle: wer hält die Referenz auf wen
| Aktion | Wer hält die Referenz | Kann Speicher freigegeben werden? |
|---|---|---|
| Subscription auf Event (+=) | Publisher auf Subscriber | Nein, solange der Publisher lebt |
| Unsubscription (-=) | Nein | Ja, nach Entfernen aller externen Referenzen |
| Keine Subscription | Nein | Ja |
3. Wie man die Abmeldung richtig organisiert
Explizites Entfernen des Handlers
Das kann man z. B. beim Schließen eines Fensters oder Formulars machen:
class SettingsForm
{
private readonly Window _window;
public SettingsForm(Window window)
{
_window = window;
_window.Updated += OnWindowUpdated;
}
public void Close()
{
_window.Updated -= OnWindowUpdated; // melden wir uns ab!
// hier kommt der Code zum Schließen (z. B. Dispose, GC.SuppressFinalize usw.)
}
public void OnWindowUpdated(object sender, EventArgs e)
{
// Event-Handling
}
}
Wenn SettingsForm via "Close"-Button zerstört wird, ist es wichtig, die Methode aufzurufen, in der die Abmeldung stattfindet (z. B. Close()).
Verwendung des Interfaces IDisposable
Für komplexe Objekte, die sich auf Events anmelden und ihren Lebenszyklus kontrollieren, ist es praktisch, IDisposable zu implementieren. In der Methode Dispose() werden alle nötigen Abmeldungen durchgeführt.
class SettingsForm : IDisposable
{
private readonly Window _window;
public SettingsForm(Window window)
{
_window = window;
_window.Updated += OnWindowUpdated;
}
public void OnWindowUpdated(object sender, EventArgs e)
{
// ...
}
public void Dispose()
{
_window.Updated -= OnWindowUpdated;
// Hier auch andere Ressourcen freigeben
}
}
Nun kann SettingsForm innerhalb eines using-Blocks verwendet, man kann Dispose() explizit aufrufen oder die Ressourcenfreigabe automatisieren (z. B. über GC.SuppressFinalize in finalisierbaren Typen).
4. Interaktion mit Lambda-Ausdrücken: Gefahren und Hacks
Wenn du dich mit einer Lambda auf ein Event anmeldest, aber die Lambda nicht in einer Variable speicherst, kannst du dich nicht wieder abmelden!
// Subscription — anonyme Lambda
window.Updated += (s, e) => Console.WriteLine("Lambda aufgerufen!");
// Wie meldest du dich jetzt ab? — Gar nicht!
window.Updated -= (s, e) => Console.WriteLine("Lambda aufgerufen!"); // Das ist ein anderer Delegate!
Was tun?
Speichere die Lambda in einer Delegate-Variablen:
EventHandler handler = (s, e) => Console.WriteLine("Lambda aufgerufen!");
window.Updated += handler;
// ... jetzt kannst du dich abmelden!
window.Updated -= handler;
5. Nützliche Feinheiten
Besonderheiten mit Lebenszyklen von Objekten und Events
Ein weiteres häufiges Problem sind zyklische Referenzen über Events zwischen zwei "long-lived" Objekten. Zum Beispiel ist ein Fenster auf das Event eines anderen registriert, beide werden lange genutzt und nicht gelöscht — und der Speicherverbrauch wächst.
Empfehlung: Versuche immer im Kopf zu behalten, wer sich bei wem anmeldet und wann eine Abmeldung nötig ist. Wenn die Subscription denselben Lebenszyklus wie der Publisher hat — okay. Wenn der Subscriber kürzer leben kann als der Publisher, implementiere eine explizite Abmeldung.
Universelle Regel: "Wenn du dich anmeldest — melde dich wieder ab!"
- Für langlebige Publisher (z. B. globale Singletons, Hauptfenster) — implementiere immer eine Abmeldung in den Subscribern.
- Für temporäre Objekte (z. B. einmalige Notifications oder Fälle, in denen der Subscriber kürzer lebt als der Publisher) — kann man lockerer sein, aber behalte den Kontext im Auge.
- Verwende Ansätze wie WeakEvent (weak events) oder spezielle Frameworks, wenn du das manuelle Abmelden vermeiden möchtest.
6. Typische Fehler beim Abmelden
Fehlerhafte Abmeldung: der Handler muss derselbe sein
Es ist sehr wichtig, dass du beim Abmelden genau denselben Handler angibst, den du bei der Anmeldung verwendet hast. Ansonsten funktioniert die Abmeldung nicht.
Fehler:
window.Updated += settingsForm.OnWindowUpdated;
// ...
window.Updated -= new SettingsForm().OnWindowUpdated; // Funktioniert nicht! Das ist eine andere Instanz und ein anderer Delegate!
Richtig:
window.Updated -= settingsForm.OnWindowUpdated;
Wenn bei der Subscription eine anonyme Lambda verwendet wird, ohne die Referenz zu speichern, ist ein Abmelden unmöglich, weil es sich dann um eine andere Delegate-Instanz handelt:
// Subscription
window.Updated += (s, e) => Console.WriteLine("Lambda!");
// Versuch der Abmeldung — funktioniert nicht!
window.Updated -= (s, e) => Console.WriteLine("Lambda!");
Die "vergessene" Abmeldung
Sehr oft wird die Abmeldung einfach vergessen, besonders wenn der Subscriber länger lebt als der Publisher oder wenn der Entwickler den Mechanismus von Events nicht vollständig verstanden hat. In der Folge bleiben Subscriber-Objekte länger im Speicher als nötig, was zu Memory Leaks und Performance-Problemen führt.
GO TO FULL VERSION