Menu

C# Func und Action: generische Delegates, Predicate und Beispiele

Wie Func und Action in C# funktionieren: Func<T, TResult> für Funktionen mit Rückgabewert, Action<T> für solche ohne, Predicate und Comparison, Funktionen übergeben und zurückgeben, ein Dictionary von Befehlen, async-Lambdas und wann du stattdessen einen eigenen Delegate deklarierst.

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

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

TypParameterRückgabe
Actionkeinevoid
Action<T>Tvoid
Action<T1, T2> ... bis 16T1, T2, ...void
Func<TResult>keineTResult
Func<T, TResult>TTResult
Func<T1, T2, TResult> ... bis 16T1, T2, ...TResult
Predicate<T>Tbool
Comparison<T>T, Tint (negativ, null, positiv)
Converter<TIn, TOut>TInTOut

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, out oder in hat. Func kann sie nicht ausdrücken, eine Funktion im Stil von TryParse braucht also delegate 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 als Func<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);.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S