Ein Delegate ist ein Typ, der eine Methodensignatur darstellt. Eine Variable eines Delegate-Typs enthält eine Referenz auf eine Methode mit dieser Signatur, und der Aufruf der Variable ruft die Methode auf. So behandelt C# Funktionen als Werte: Du kannst sie speichern, an andere Methoden übergeben und später aufrufen.
Ausgabe:
40
70
68.00
delegate decimal PriceRule(decimal price); deklariert einen neuen Typ, genau wie class. Jede Methode, die ein decimal nimmt und ein decimal zurückgibt, passt dazu. Die Variable rule zeigte nacheinander auf drei verschiedene Methoden, und jeder Aufruf führte die aus, die sie gerade enthielt.
Deklarieren, erzeugen und aufrufen
Eine Delegate-Typdeklaration sieht aus wie eine Methodensignatur mit dem Schlüsselwort delegate davor. Sie steht meist auf Namespace-Ebene neben Klassen oder verschachtelt in einer Klasse.
Es gibt vier Wege, eine Delegate-Instanz zu erzeugen:
PriceRule a = HalfPrice; // method group (most common)
PriceRule b = new PriceRule(HalfPrice); // explicit constructor, same thing
PriceRule c = p => p - 5m; // lambda expression
PriceRule d = delegate (decimal p) { return p; }; // anonymous method (C# 2 syntax)
Und zwei Wege, sie aufzurufen, die identisch sind: rule(80m) und rule.Invoke(80m). Der Compiler prüft die Signatur bei der Zuweisung, eine unpassende Methode ist also ein Kompilierfehler und keine Überraschung zur Laufzeit.
Ein Delegate kann auch auf eine Instanzmethode zeigen. Er merkt sich dann sowohl die Methode als auch das Objekt, auf dem sie aufgerufen wird:
Ausgabe:
Ana got: Your order shipped
target: Mailbox, method: Receive
Target ist das Objekt, auf dem der Delegate die Methode aufruft (null bei einer statischen Methode), und Method beschreibt die Methode. Das Festhalten des Targets bedeutet auch, dass ein Delegate dieses Objekt am Leben hält, was bei Events eine Rolle spielt.
Delegates als Callbacks
Die Hauptaufgabe eines Delegates ist, eine Methode Code aufrufen zu lassen, den ihr Aufrufer ausgewählt hat. Die Methode legt fest, wann etwas passiert; der Aufrufer legt fest, was:
Ausgabe:
[## ] 25%
[##### ] 50%
[####### ] 75%
[##########] 100%
import finished
ImportRows weiß nichts von Konsolen oder Balken. Dieselbe Methode kann einen Fortschrittsbalken, eine Logzeile oder eine UI-Aktualisierung steuern, je nachdem, was der Aufrufer übergibt. List.Sort(Comparison<T>), Array.Find(Predicate<T>), Task.Run(Action) und jeder LINQ-Operator folgen diesem Muster.
Multicast-Delegates: += und -=
Ein Delegate kann mehr als eine Methode enthalten. += hängt eine Methode an seine Aufrufliste an, -= entfernt sie, und der Aufruf des Delegates ruft jede Methode der Reihe nach auf:
Ausgabe:
email sent for A-1001
stock updated for A-1001
warehouse notified for A-1001
3 handlers
email sent for A-1002
warehouse notified for A-1002
Drei Details von Multicast-Delegates zählen in der Praxis:
- Rückgabewerte: Gibt der Delegate-Typ einen Wert zurück, liefert der Aufruf eines Multicast-Delegates nur das Ergebnis der letzten Methode. Um alle Ergebnisse zu sammeln, durchlaufe
GetInvocationList()und rufe jede Methode selbst auf. - Exceptions: Wirft eine Methode, laufen die übrigen Methoden nicht, und die Exception erreicht den Aufrufer.
- Unveränderlichkeit: Delegates sind unveränderlich.
+=erzeugt einen neuen Delegate mit längerer Liste und weist ihn der Variable zu, deshalb wirkt sich ein-=auf eine Kopie nicht auf das Original aus.
Ein Lambda mit -= zu entfernen funktioniert nur, wenn du dieselbe Delegate-Instanz übergibst. Dasselbe Lambda zweimal zu schreiben erzeugt zwei verschiedene Delegates, handlers -= id => Log(id); entfernt also nichts. Speichere das Lambda in einer Variable, wenn du es später abmelden willst.
null-Delegates und ?.Invoke
Eine Delegate-Variable ohne Zuweisung ist null, und das Entfernen der letzten Methode mit -= hinterlässt ebenfalls null. Der Aufruf von null wirft NullReferenceException:
Ausgabe:
no handler, no crash
saved draft.txt
NullReferenceException
?.Invoke ist der Standardweg, einen optionalen Callback aufzurufen. Der Operator ?. kann nicht direkt vor die Klammern gesetzt werden (onSaved?("x") ist keine gültige Syntax), daher ruft das Muster die Methode Invoke des Delegates beim Namen auf, was dasselbe tut wie der Kurzaufruf.
Delegates, Func, Action und Events
Für jede Signatur einen Delegate-Typ zu deklarieren wäre mühsam, deshalb liefert .NET generische mit:
| Eingebauter Typ | Entsprechende eigene Deklaration |
|---|---|
Action | delegate void Action(); |
Action<T> | delegate void Action<T>(T arg); |
Func<TResult> | delegate TResult Func<TResult>(); |
Func<T, TResult> | delegate TResult Func<T, TResult>(T arg); |
Predicate<T> | delegate bool Predicate<T>(T obj); |
PriceRule aus dem ersten Beispiel hat dieselbe Form wie Func<decimal, decimal>, trotzdem sind beide verschiedene Typen und lassen sich nicht ineinander umwandeln. Selbst zwei eingebaute Typen mit identischer Signatur lassen sich das nicht:
Func<int, bool> isPositive = x => x > 0;
Predicate<int> p = isPositive;
// error CS0029: Cannot implicitly convert type 'System.Func<int, bool>' to 'System.Predicate<int>'
Predicate<int> ok = new Predicate<int>(isPositive); // wrap it explicitly
Predicate<int> ok2 = x => isPositive(x); // or with a lambda
Ein Lambda lässt sich in jeden kompatiblen Delegate-Typ umwandeln, APIs nehmen daher meist Lambdas, und die Frage kommt gar nicht auf. Die Seite zu Func und Action behandelt die generische Familie und wann sich eine eigene Deklaration trotzdem lohnt.
Events bauen direkt auf Delegates auf. Ein event-Feld ist ein Multicast-Delegate, bei dem externer Code nur += und -= verwenden darf, ihn aber nie aufrufen oder überschreiben kann. Die Abonnentenliste im Multicast-Beispiel oben ist genau das, was ein Event für dich verwaltet, mit diesem zusätzlichen Schutz. Events haben eine eigene Seite.
Häufig gestellte Fragen
Was ist ein Delegate in C#?
Ein Delegate ist eine typsichere Referenz auf eine Methode. Du deklarierst einen Delegate-Typ mit einer Signatur, etwa delegate decimal PriceRule(decimal price);, und jede Methode mit dieser Signatur (statisch, Instanzmethode oder ein Lambda) kann in einer Variable dieses Typs gespeichert und später darüber aufgerufen werden. Mit Delegates übergibt C# Funktionen als Werte.
Was ist der Unterschied zwischen einem Delegate und Func in C#?
Func<T, TResult> und Action<T> sind Delegate-Typen, die .NET bereits für dich deklariert, generisch über Parameter- und Rückgabetypen. Eine eigene delegate-Deklaration gibt dem Typ einen Namen und Parameternamen, die seine Rolle dokumentieren, und unterstützt Parameter mit ref, out und in. Für den meisten Code reichen Func und Action.
Was ist ein Multicast-Delegate in C#?
Ein Delegate, der mehrere Methoden enthält. += fügt seiner Aufrufliste eine Methode hinzu und -= entfernt eine; der Aufruf des Delegates ruft sie alle der Reihe nach auf. Gibt der Delegate einen Wert zurück, bekommt der Aufrufer nur das Ergebnis der letzten Methode, und eine Exception in einer Methode verhindert, dass die übrigen laufen.
Wie rufe ich einen Delegate sicher auf, der null sein könnte?
Mit dem Null-bedingten Operator: onProgress?.Invoke(50);. Eine Delegate-Variable ohne zugewiesene Methoden ist null, und ein direkter Aufruf wirft NullReferenceException. ?.Invoke liest die Variable einmal und ist daher auch sicher, wenn ein anderer Thread im selben Moment den letzten Handler entfernt.
Wann sollte ich einen eigenen Delegate-Typ statt Func oder Action deklarieren?
Wenn die Signatur Parameter mit ref, out oder in braucht (Func und Action können sie nicht ausdrücken), wenn ein benannter Typ eine öffentliche API klarer macht (delegate bool Validator(string input)) oder wenn Parameternamen in IntelliSense erscheinen sollen. Ansonsten bevorzuge Func und Action, die jeder .NET-Entwickler kennt.