Menu

C# Events: das Schlüsselwort event, EventHandler, abonnieren und auslösen

Wie Events in C# funktionieren: ein Event mit EventHandler und EventHandler<T> deklarieren, eigene EventArgs-Klassen, mit += und -= abonnieren und abmelden, ein Event sicher mit ?.Invoke auslösen, warum ein Event besser ist als ein öffentliches Delegate-Feld und das Speicherleck durch ein vergessenes Abmelden.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Mit einem Event kann eine Klasse ankündigen, dass etwas passiert ist, ohne zu wissen, wer zuhört. Andere Objekte abonnieren mit +=, und wenn die Klasse das Event auslöst, läuft die Methode jedes Abonnenten.

Ausgabe:

Order A-17 saved
  receipt emailed for A-17
Order A-18 saved
  receipt emailed for A-18
  free gift added to A-18

Shop weiß nicht, dass es Quittungen oder Geschenke gibt. Es löst OrderPlaced aus, und wer immer abonniert hat, reagiert. Eine neue Reaktion (Treuepunkte, Analytics) bedeutet einen weiteren Abonnenten, keine Änderung an PlaceOrder.

Ein Event deklarieren

Eine Event-Deklaration ist ein Member mit Delegate-Typ und dem Schlüsselwort event:

public event EventHandler<OrderPlacedEventArgs> OrderPlaced;

Der Delegate-Typ legt die Signatur des Handlers fest. .NET-Code verwendet fast immer einen von zwei eingebauten Typen, und wer der Konvention folgt, macht seine Events für jeden anderen .NET-Entwickler vertraut:

  • EventHandler: void (object sender, EventArgs e), für Events ohne Daten. Löse es mit EventArgs.Empty aus.
  • EventHandler<TEventArgs>: void (object sender, TEventArgs e), wobei TEventArgs deine Klasse mit den Daten ist.

Die Konventionen drumherum:

  • sender ist das Objekt, das das Event ausgelöst hat, meist this.
  • Die Datenklasse heißt {Something}EventArgs, leitet von EventArgs ab (seit .NET 4.5 optional, aber weiterhin üblich) und stellt schreibgeschützte Properties bereit.
  • Das Event wird nach dem benannt, was passiert ist: OrderPlaced, Closed, PriceChanged. Ein Name auf ...ing (Closing) wird für Events verwendet, die vor der Aktion ausgelöst werden, oft mit einer Möglichkeit, sie abzubrechen.
  • In Klassen, von denen geerbt werden soll, läuft das Auslösen über eine Methode protected virtual void OnOrderPlaced(OrderPlacedEventArgs e), damit abgeleitete Klassen sich einklinken können.

Abonnieren und abmelden

+= fügt einen Handler hinzu und -= entfernt ihn. Ein Handler kann eine Methode oder ein Lambda sein. Um ein Lambda später zu entfernen, speichere es in einer Variable, denn dasselbe Lambda erneut zu schreiben erzeugt einen anderen Delegate, den -= nicht findet:

Ausgabe:

set to 32
  display shows 32 C
  alarm: too hot
set to 35
  display shows 35 C
set to 20
  display shows 20 C

Einen Handler zu entfernen, der nie hinzugefügt wurde, ist kein Fehler; es passiert einfach nichts. EventHandler<int> zeigt, dass das Typargument in modernem .NET nicht von EventArgs abgeleitet sein muss, auch wenn eine eigene Klasse Raum lässt, später Felder hinzuzufügen, ohne Abonnenten zu brechen.

Handler laufen synchron, in der Reihenfolge des Abonnierens, auf dem Thread, der das Event ausgelöst hat. Wirft ein Handler, laufen die übrigen nicht, und die Exception gelangt zu dem Code, der das Event ausgelöst hat.

Ein Event sicher auslösen

Ein Event ohne Abonnenten enthält null. Ein direkter Aufruf würde NullReferenceException werfen, löse es also mit dem Null-bedingten Operator aus:

OrderPlaced?.Invoke(this, args);

Vor C# 6 bestand das Muster darin, das Feld in eine lokale Variable zu kopieren, sie zu prüfen und die Kopie aufzurufen. Die Kopie zählt in Multithreading-Code: OrderPlaced != null zu prüfen und dann OrderPlaced(...) aufzurufen liest das Feld zweimal, und ein anderer Thread könnte dazwischen den letzten Handler entfernen. ?. liest es einmal und ist daher kürzer und korrekt.

Warum event und kein öffentliches Delegate-Feld

Ohne das Schlüsselwort event funktioniert ein öffentliches Delegate-Feld zum Abonnieren, gibt aber jedem Abonnenten die volle Kontrolle:

Ausgabe:

analytics ping
analytics ping

Ein einziges = statt += hat die beiden vorherigen Handler stillschweigend entfernt, und externer Code konnte den „Klick“ auslösen, ohne dass geklickt wurde. Wird das Feld als event markiert, sind beide Zeilen Kompilierfehler:

error CS0070: The event 'Button.Clicked' can only appear on the left hand side of += or -= (except when used from within the type 'Button')

Innerhalb von Button verhält sich das Event weiterhin wie ein normales Delegate-Feld, die Klasse kann es also aufrufen und auf null prüfen.

Das Leck durch vergessenes Abmelden

Beim Abonnieren speichert das Event einen Delegate, und der Delegate hält eine Referenz auf das abonnierende Objekt. Solange der Publisher lebt und der Handler angehängt ist, kann der Abonnent nicht vom Garbage Collector eingesammelt werden, und er bekommt weiter Events, nachdem du mit ihm fertig bist:

Ausgabe:

  closed widget shows 101.5
  open widget shows 101.5
subscribers left: 1
  closed widget shows 99.0

leaky auf null zu setzen hat für den Feed nichts bewirkt: Seine Handlerliste verweist noch auf dieses Widget, das „geschlossene“ Widget aktualisiert sich also weiter und bleibt im Speicher. Das Widget im using-Block hat sich in Dispose abgemeldet und keine Events mehr bekommen. Das ist eines der häufigsten Speicherlecks in .NET-Desktop- und Servercode, besonders bei statischen Events und anwendungsweiten Diensten, die bis zum Ende des Prozesses leben.

Die Regel: Wer ein Event auf einem Objekt abonniert, das länger lebt als er selbst, muss sich abmelden, meist in Dispose (siehe die using-Anweisung). Haben Publisher und Abonnent dieselbe Lebensdauer, etwa ein Formular und seine eigenen Buttons, ist kein Abmelden nötig.

Eigene add- und remove-Accessoren

Ein Event kann festlegen, was += und -= tun, wie get und set bei einer Property. Das ist selten, aber so speichern Frameworks Handler in einem gemeinsamen Dictionary oder leiten sie an ein anderes Objekt weiter:

private EventHandler closed;

public event EventHandler Closed
{
    add    { Console.WriteLine("subscriber added");   closed += value; }
    remove { Console.WriteLine("subscriber removed"); closed -= value; }
}

Mit eigenen Accessoren löst die Klasse das Event über das Hintergrundfeld aus (closed?.Invoke(this, EventArgs.Empty)), da das Event selbst keinen Speicher mehr hat.

Häufig gestellte Fragen

Was ist ein Event in C#?

Ein Event ist ein Klassenmember, über das andere Objekte darum bitten können, benachrichtigt zu werden, wenn etwas passiert. Dahinter steht ein Multicast-Delegate, aber externer Code kann nur Handler hinzufügen (+=) und entfernen (-=). Nur die Klasse, die das Event deklariert, kann es auslösen.

Was ist der Unterschied zwischen einem Event und einem Delegate in C#?

Ein Delegate ist ein Typ, der auf Methoden verweist. Ein Event ist ein Member, das mit einem Delegate-Typ plus dem Schlüsselwort event deklariert wird, das den Zugriff einschränkt: Von außerhalb der Klasse kannst du es nicht aufrufen, seine Handlerliste nicht lesen und es nicht mit = zuweisen, nur abonnieren und abmelden. Ein öffentliches Delegate-Feld erlaubt all das, jeder Abonnent könnte also die anderen löschen oder das Event auslösen.

Wie übergebe ich Daten mit einem C#-Event?

Leite eine Klasse von EventArgs ab, mit den Daten als schreibgeschützte Properties (public class OrderPlacedEventArgs : EventArgs { public decimal Total { get; } }), und deklariere das Event als EventHandler<OrderPlacedEventArgs>. Löse es mit OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(total)); aus.

Wie löse ich in C# ein Event sicher aus?

Mit MyEvent?.Invoke(this, args);. Ein Event ohne Abonnenten ist null, ein direkter Aufruf wirft also NullReferenceException. Der Operator ?. liest das Feld einmal, was auch eine Race Condition vermeidet, bei der ein anderer Thread den letzten Handler zwischen einer Null-Prüfung und dem Aufruf entfernt.

Können C#-Events Speicherlecks verursachen?

Ja. Beim Abonnieren wird ein Delegate gespeichert, der auf den Abonnenten verweist, der Publisher hält den Abonnenten also so lange am Leben wie sich selbst. Ein langlebiger Publisher (ein statisches Event, ein anwendungsweiter Dienst) mit kurzlebigen Abonnenten, die sich nie abmelden, hält jeden von ihnen im Speicher. Melde dich mit -= ab, wenn der Abonnent fertig ist, typischerweise in Dispose.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S