Func und Action sind generische Delegate-Typen, die .NET für dich deklariert. Du kannst also Funktionen speichern und übergeben, ohne vorher einen Delegate-Typ zu deklarieren. Func gibt einen Wert zurück; Action gibt nichts zurück.
Ausgabe:
4
46.00
False
Hello!
Mia scored 87
Die Regel zum Merken: Bei einem Func ist das letzte Typargument der Rückgabetyp. Func<int, int, bool> nimmt zwei ints und gibt ein bool zurück. Func<int> nimmt nichts und gibt ein int zurück. Die Typargumente einer Action sind alle Parameter.
Die Familie
| Typ | Parameter | Rückgabe |
|---|---|---|
Action | keine | void |
Action<T> | T | void |
Action<T1, T2> ... bis 16 | T1, T2, ... | void |
Func<TResult> | keine | TResult |
Func<T, TResult> | T | TResult |
Func<T1, T2, TResult> ... bis 16 | T1, T2, ... | TResult |
Predicate<T> | T | bool |
Comparison<T> | T, T | int (negativ, null, positiv) |
Converter<TIn, TOut> | TIn | TOut |
Predicate, Comparison und Converter sind älter und kommen in Methoden von List<T> und Array vor: FindAll(Predicate<T>), Sort(Comparison<T>), ConvertAll(Converter<T, TOut>). LINQ verwendet durchgehend Func, deshalb nehmen list.Where(...) und list.FindAll(...) Lambdas derselben Form, aber verschiedener Typen:
Ausgabe:
17, 15
34, 52, 28
52, 34, 28, 17, 15
52y 34y 28y 17y 15y
Funktionen an Methoden übergeben
Ein Parameter vom Typ Func oder Action lässt den Aufrufer den Teil eines Algorithmus liefern, der variiert. Zwei häufige Formen sind „führe das aus und miss es“ und „versuche das, bis es klappt“:
Ausgabe:
attempt 1 failed
attempt 2 failed
<html>ok</html>
clicks: 2
Retry ist generisch, sein Func<int, T> kann also alles zurückgeben; der Compiler hat T aus dem Lambda als string abgeleitet. Das an Twice übergebene Lambda ändert die lokale Variable clicks, was funktioniert, weil ein Lambda die Variable selbst erfasst, keine Kopie ihres Werts.
Methodengruppen: eine Methode per Namen übergeben
Du brauchst kein Lambda, um ein Func oder eine Action zu füllen. Der Name einer vorhandenen Methode mit passender Signatur (eine „Methodengruppe“) lässt sich direkt umwandeln:
Ausgabe:
parsed: 43
sat is weekend: True
total quantity: 20
Select(int.Parse) ist dasselbe wie Select(s => int.Parse(s)), nur mit einer Aufrufebene weniger. Der Compiler wählt die Überladung, die zum Zieltyp passt: int.Parse hat mehrere Überladungen, und nur Parse(string) passt zu Func<string, int>. Wenn der Compiler sich nicht auf eine festlegen kann (mehrere Überladungen passen gleich gut, oder die Typargumente einer generischen Methode lassen sich aus einer Methodengruppe nicht ableiten), kompiliert der Aufruf nicht, und ein Lambda, das den Aufruf explizit macht, behebt das.
Eine als Func verwendete Methodengruppe läuft trotzdem später, wann immer der Delegate aufgerufen wird, genau wie ein Lambda. Der Unterschied liegt nur in der Schreibweise.
Funktionen zurückgeben
Eine Methode kann eine Funktion bauen und zurückgeben. Das zurückgegebene Func trägt die Werte, aus denen es gebaut wurde:
Ausgabe:
140
180
162
Twice(staff) baut eine neue Funktion, die den Mitarbeiterrabatt zweimal anwendet: 200, dann 180, dann 162.
Ein Dictionary von Funktionen
Funktionen in einem Dictionary zu speichern macht aus einem langen switch eine Nachschlagetabelle. Das ist die übliche Form für Befehls-Handler, Rechner und Menüaktionen, und neue Einträge lassen sich zur Laufzeit hinzufügen:
Ausgabe:
12 + 30 = 42
7 * 6 = 42
9 / 0 = cannot divide by zero
2 ^ 3 = unknown operator
Hello, Sam
DONE
Ein neuer Operator ist ein weiterer Dictionary-Eintrag, ohne Änderung an der Schleife. Der an das zweite Dictionary übergebene StringComparer.OrdinalIgnoreCase macht die Befehlsnamen unabhängig von Groß- und Kleinschreibung.
Async-Funktionen: Func<Task>, nicht Action
Bei async-Code entscheidet der Delegate-Typ, ob der Aufrufer auf die Arbeit warten kann:
Func<Task> save = async () => await File.WriteAllTextAsync("a.txt", "data");
await save(); // the caller can await it and see its exceptions
Func<int, Task<string>> load = async id => await FetchUserAsync(id);
string user = await load(42);
Action bad = async () => await File.WriteAllTextAsync("a.txt", "data");
bad(); // async void: fire and forget, exceptions escape
Ein async-Lambda, das einer Action zugewiesen wird, wird zu einer async void-Methode: Nichts kann es abwarten, und eine Exception darin erreicht nie den Aufrufer. Sie wird im Thread-Pool erneut geworfen (oder im UI-Thread einer Desktop-App), wo sie meist den Prozess abstürzen lässt. Wenn du eine Methode schreibst, die async-Arbeit annimmt, nimm ein Func<Task> (oder Func<Task<T>>).
Wann du einen eigenen Delegate deklarierst
Func und Action decken die meisten Fälle ab. Deklariere einen benannten Delegate-Typ, wenn:
- Die Signatur Parameter mit
ref,outoderinhat.Funckann sie nicht ausdrücken, eine Funktion im Stil vonTryParsebraucht alsodelegate bool TryParser<T>(string text, out T value);. - Der Name eine Rolle in einer öffentlichen API dokumentiert.
delegate bool Validator(string input)in einer Methodensignatur sagt mehr alsFunc<string, bool>, und seine Parameternamen erscheinen in IntelliSense. - Du einen
params-Parameter brauchst, den generische Delegates ebenfalls nicht haben können.
Events sind die andere Stelle, an der eigene Delegate-Typen noch auftauchen, auch wenn das eingebaute EventHandler<TEventArgs> die meisten abdeckt. Die Seite zu Delegates behandelt das Deklarieren und Kombinieren von Delegate-Typen.
Häufig gestellte Fragen
Was ist der Unterschied zwischen Func und Action in C#?
Func gibt einen Wert zurück, Action nicht. In Func<int, string, bool> ist das letzte Typargument (bool) der Rückgabetyp, die anderen sind Parameter. Action<int, string> nimmt ein int und einen string und gibt void zurück. Beide gibt es in Versionen mit 0 bis 16 Parametern.
Wie übergebe ich in C# eine Funktion als Parameter?
Deklariere den Parameter als Func oder Action der passenden Form und rufe ihn in der Methode auf: static decimal Apply(decimal price, Func<decimal, decimal> rule) => rule(price);. Aufrufer übergeben ein Lambda (Apply(80m, p => p * 0.9m)) oder einen Methodennamen (Apply(80m, HalfPrice)).
Was ist Predicate<T> in C#?
Ein Delegate-Typ, der ein T nimmt und bool zurückgibt, dieselbe Form wie Func<T, bool>. Er ist älter als Func und wird von List<T>.Find, FindAll, RemoveAll, Exists und Array.Find verwendet. Die beiden Typen lassen sich nicht ineinander umwandeln, aber ein Lambda lässt sich in jeden der beiden umwandeln.
Wie verwende ich Func mit async-Code?
Nimm Func<Task> für eine async-Funktion ohne Ergebnis und Func<T, Task<TResult>> für eine mit Ergebnis, damit der Aufrufer sie mit await abwarten kann. Weise ein async-Lambda nicht einer Action zu: Das macht es zu async void, das sich nicht abwarten lässt und dessen Exceptions der Aufrufer nicht abfangen kann.
Kann Func out- oder ref-Parameter haben?
Nein. Die generischen Parameter von Func und Action sind gewöhnliche Wertparameter, Func<string, out int, bool> lässt sich also nicht schreiben. Für eine Signatur wie TryParse deklariere einen eigenen Delegate: delegate bool TryParser<T>(string text, out T value);.