In den meisten Sprachen ist Observer ein Pattern, das man von Hand baut: eine Liste von Beobachtern, eine Registriere-Methode, eine Schleife beim Benachrichtigen. In C# gibt es das als Schlüsselwort.
public sealed class BestandArgs : EventArgs
{
public required string Artikel { get; init; }
public required int Bestand { get; init; }
}
public sealed class Lager
{
private readonly Dictionary<string, int> _bestand = new();
public event EventHandler<BestandArgs>? MindestbestandUnterschritten;
public void Entnehme(string artikel, int menge)
{
_bestand[artikel] -= menge;
if (_bestand[artikel] < 10)
{
MindestbestandUnterschritten?.Invoke(
this,
new BestandArgs { Artikel = artikel, Bestand = _bestand[artikel] });
}
}
}Lager kennt jetzt niemanden mehr. Der Konstruktor ist leer, die Testbarkeit ist zurück, und die Klasse macht wieder eine Sache.
Die Empfänger melden sich an:
var lager = new Lager();
lager.MindestbestandUnterschritten += (_, e) => nachbestellung.Beauftrage(e.Artikel);
lager.MindestbestandUnterschritten += (_, e) => dashboard.Aktualisiere(e.Artikel, e.Bestand);
lager.MindestbestandUnterschritten += (_, e) => logger.LogWarning(
"Bestand {Artikel} auf {Bestand}", e.Artikel, e.Bestand);Warum `?.Invoke` und nicht einfach der Aufruf
Ein Event ohne einen einzigen Abonnenten ist null. Ohne das ?. bekämst du eine NullReferenceException, sobald niemand zuhört — also im häufigsten Fall überhaupt. Seit C# 6 ist ?.Invoke(...) die übliche Schreibweise dafür.
Was event gegenüber einem einfachen Delegate-Feld bringt
Ein public Action<BestandArgs> Unterschritten; würde technisch dasselbe tun. Das Schlüsselwort event schränkt aber ein, was von aussen erlaubt ist:
Öffentliches Delegate-Feld vs. `event`
Feld — von aussen alles erlaubt
public Action<BestandArgs>? Unterschritten;
// Anderswo im Code:
lager.Unterschritten = meinHandler;
// ^ überschreibt alle anderen Abonnenten
lager.Unterschritten(args);
// ^ Fremde können das Ereignis auslösenevent — nur +=, -= von aussen
public event EventHandler<BestandArgs>? Unterschritten;
lager.Unterschritten = meinHandler;
// ^ Compilerfehler
lager.Unterschritten(args);
// ^ Compilerfehler — auslösen darf
// nur die deklarierende KlasseEin event ist ein Delegate-Feld, dem der Compiler zwei Rechte entzieht: Zuweisen und Auslösen. Genau diese zwei sind die, die von aussen niemandem zustehen.
Die andere Bauform: IObservable<T>
Neben Events gibt es in .NET das Paar IObservable<T> / IObserver<T>. Es ist die formalere Umsetzung desselben Patterns und kann drei Dinge, die ein Event nicht kann:
- Ein Ende melden (
OnCompleted) — die Quelle ist versiegt. - Einen Fehler melden (
OnError) — die Quelle ist gescheitert. - Das Abmelden gibt ein
IDisposablezurück, statt ein-=mit demselben Delegate zu verlangen.
Der Grund, es zu nehmen, ist selten das Interface selbst — es ist Rx (System.Reactive), das darauf Operatoren aufsetzt:
// Ereignisse zusammenfassen, statt bei jedem einzelnen zu reagieren
messwerte
.Where(m => m.Wert > grenzwert)
.Throttle(TimeSpan.FromSeconds(5))
.DistinctUntilChanged(m => m.SensorId)
.Subscribe(m => alarm.Ausloesen(m));Womit anfangen
Nimm event als Standard — es ist eingebaut, jeder C#-Entwickler kennt es, und für «etwas ist passiert» reicht es. Greif zu Rx, wenn du Ereignisse über die Zeit verarbeitest: entprellen, zusammenfassen, aus mehreren Quellen zusammenführen.
Kurz nachgedacht
Zählt nicht — nur zum Prüfen, ob es angekommen ist.
1.Warum ruft man ein Event mit ?.Invoke(...) auf?