Lambda-Ausdrücke, Stellvertretungen und Ereignisse

Tip

Neu bei der Entwicklung von Software? Beginnen Sie zuerst mit den Lernprogrammen " Erste Schritte ". Bauen Sie zunächst Kernkompetenzen in Typen und Methoden auf, bevor Sie Lambda-Ausdrücke verwenden.

Manchmal möchten Sie ein kleines Verhalten, eine Funktion, direkt an eine andere Methode übergeben. Sie können beispielsweise eine Liste filtern, aber die Filterbedingung ändert sich je nach Situation. Anstatt eine separate benannte Methode für jede mögliche Bedingung zu schreiben, übergeben Sie die Bedingung selbst als Argument.

Lambda-Ausdrücke sind das C#-Feature, das dies ermöglicht. Ein Lambda-Ausdruck ist eine kompakte Inlinefunktion, die Sie schreiben, ohne ihm einen Namen zu geben. Sie verwenden den Pfeiloperator => , um die Parameterliste vom Textkörper zu trennen:

x => x * 2

Von links nach rechts: x ist der Eingabeparameter, => bedeutet „Geht an”, und x * 2 ist der Textkörper. Er berechnet den zurückgegebenen Wert. Wenn keine Parameter oder mehrere Parameter vorhanden sind, schließen Sie sie in Klammern ein: () => 42 oder (left, right) => left + right.

Delegaten unterstützen Lambda-Ausdrücke

Um einen Lambda-Ausdruck zu verwenden, muss der C#-Compiler zwei Dinge kennen: die Typen der Parameter und den Rückgabetyp. Diese Beschreibung, die Parametertypen plus Rückgabetyp, wird als Delegattyp bezeichnet.

Ein Delegattyp ist ein Typ, der eine Methodensignatur darstellt. Eine Variable eines Delegattyps kann eine beliebige passende Methode, wie einen Lambda-Ausdruck oder eine benannte Methode, enthalten, sofern die Parametertypen und die Rückgabetypen übereinstimmen.

Sie deklarieren einen Delegattyp mit dem delegate Schlüsselwort:

delegate int Transform(int value);

Diese Deklaration besagt: "Transform ist ein Delegattyp für Methoden, die einen int akzeptieren und einen intzurückgeben." Anschließend können Sie einer Variablen dieses Typs einen Lambda-Ausdruck oder eine benannte Methode zuweisen:

Transform doubler = x => x * 2;    // assign a lambda expression
Transform squarer = Square;         // assign a named method

Console.WriteLine(doubler(5));      // 10
Console.WriteLine(squarer(5));      // 25

static int Square(int value) => value * value;

Sowohl doubler als auch squarer haben einen Wert vom Typ Transform. Sie rufen sie genau wie normale Methoden auf. Der Compiler überprüft, ob das, was Sie zuweisen, der deklarierten Signatur entspricht.

Integrierte Delegattypen: Func und Action

Das Deklarieren eines benutzerdefinierten Delegattyps für jede Situation kann sich wiederholen. .NET stellt zwei Familien generischer Delegattypen bereit, Func und Action, die die meisten Szenarien abdecken, sodass Sie das Schlüsselwort delegate nur selten selbst verwenden müssen.

Beide Familien werden in Versionen mit Null- bis Sechzehn Eingabetypparametern verwendet, sodass sie auf eine beliebige Anzahl von Eingaben skaliert werden. Der Hauptunterschied zwischen den beiden Familien ist:

  • System.Func<T,TResult> (und Func<T1, T2, TResult>usw.) stellt eine Methode dar, die einen Wert zurückgibt. Der letzte Typparameter ist immer der Rückgabetyp; alle früheren Typen sind Eingabetypen.
  • System.Action<T>(und Action<T1, T2>usw.) stellt eine Methode dar, die nichts () void. Alle Typparameter sind Eingabetypen. System.Action ohne Typparameter stellt eine Methode ohne Eingaben und keinen Rückgabewert dar.

Beschreibt beispielsweise Func<int, int, int> eine Methode mit zwei int Eingaben und einem int Ergebnis. Action<string> beschreibt eine Methode mit einer string Eingabe und keinen Rückgabewert.

Func<int, int, int> add = (left, right) => left + right;
Action<string> report = message => Console.WriteLine($"Report: {message}");

int total = add(5, 9);
report($"5 + 9 = {total}");

Verwenden Sie beschreibende Parameternamen in Lambdas, damit Leser die Absicht verstehen können, ohne den vollständigen Methodentext zu überprüfen.

Übergeben eines Lambda-Ausdrucks an eine Methode

Wenn eine Methode einen Func- oder Action-Parameter deklariert, übergeben Aufrufer einen Lambda-Ausdruck, der dem Delegaten-Typ entspricht. Der Compiler überprüft, ob die Parametertypen der Lambda-Funktion und der Rückgabetyp mit dem deklarierten Delegattyp übereinstimmen. Wenn sie nicht übereinstimmen, wird der Code nicht kompiliert.

int[] numbers = [1, 2, 3, 4, 5, 6];
int[] evenNumbers = Filter(numbers, value => value % 2 == 0).ToArray();

Console.WriteLine(string.Join(", ", evenNumbers));

Die Filter Methode deklariert einen Func<int, bool> Parameter mit dem Namen predicate. Der Func<int, bool> Typ weist Anrufer auf die erwartete Struktur hin: eine int Eingabe, ein bool Ergebnis. Der Aufrufer übergibt value => value % 2 == 0 als das Argument. Dieses Muster wird in LINQ und vielen .NET APIs angezeigt.

Beibehalten von eigenständigen Lambda-Ausdrücken

Ein Lambda-Ausdruck kann auf Variablen aus dem umgebenden Code verweisen. Erfassen bedeutet, dass das Lambda einen Verweis auf eine Variable hält, die außerhalb ihres eigenen Körpers deklariert ist. Die Kombination aus Lambda und den erfassten Variablen wird als Schließen bezeichnet.

Wenn Sie nichts erfassen müssen, fügen Sie den static Modifizierer zur Lambda-Funktion hinzu. Eine statische Lambda kann nur ihre eigenen Parameter und Werte verwenden, die in ihrem Körper deklariert sind. Lokale Variablen oder Instanzstatus können nicht aus dem eingeschlossenen Bereich erfasst werden.

Func<int, bool> isEven = static value => value % 2 == 0;

Console.WriteLine(isEven(14));
Console.WriteLine(isEven(15));

Statische Lambdas machen die Absicht deutlich und verhindern ungewollte Bindungen.

Verwerfungsparameter verwenden, wenn Eingaben irrelevant sind

Manchmal enthält eine Stellvertretungssignatur Parameter, die Sie nicht benötigen. Verwenden Sie den Ausschuss _, um diese Auswahl explizit kenntlich zu machen.

Häufige Beispiele sind Ereignishandler, bei denen Sie nicht verwenden sender oder EventArgs, Rückrufe, bei denen Sie nur einige von mehreren Eingaben benötigen, und LINQ-Überladungen, die einen Index bereitstellen, den Sie nicht verwenden.

Action<int, int, string> statusUpdate = (_, _, message) => Console.WriteLine(message);
statusUpdate(200, 42, "Operation completed");

Ausschüsse verbessern die Lesbarkeit, da sie zeigen, welche Parameter wichtig sind.

Ereignisse bieten optionale Benachrichtigungen

Ein Ereignis ist ein Mechanismus, mit dem ein Objekt ( der Herausgeber) andere Objekte (die Abonnenten) benachrichtigt, wenn etwas geschieht. Der Herausgeber muss nicht wissen, wer lauscht oder wie viele Abonnenten es gibt. Abonnenten entscheiden sich für die Anmeldung.

Ereignisse basieren auf Delegaten. Ein Ereignis ist ein Delegatfeld mit zusätzlichen Einschränkungen, die durch das event-Schlüsselwort erzwungen werden. Externer Code kann das Ereignis nur abonnieren (+=) oder sich vom Ereignis abmelden (-=); nur die Klasse, die das Ereignis deklariert, kann es aufrufen (auslösen).

Die .NET Konvention für Ereignisdelegattypen ist System.EventHandler<TEventArgs>, wobei T der Typ der in der Benachrichtigung enthaltenen Daten ist. Die Signatur hat immer zwei Parameter: das sender (Objekt, das das Ereignis ausgelöst hat) und die Ereignisdaten des Typs T.

MessagePublisher publisher = new();
publisher.MessagePublished += (_, message) => Console.WriteLine($"Received: {message}");

publisher.Publish("Records updated");

Den Code durchgehen:

  • MessagePublisher deklariert event EventHandler<string>? MessagePublished. Das event Schlüsselwort bedeutet, dass Anrufer nur abonnieren oder kündigen können – sie können sie nicht direkt aufrufen.
  • publisher.MessagePublished += (_, message) => ... abonniert das Ereignis mittels einer Lambda-Expression. Der _ verwirft den sender-Parameter, da dieser Handler ihn nicht benötigt.
  • publisher.Publish("Records updated") löst das Ereignis aus und führt jeden registrierten Handler aus.

Das Abonnieren ist optional. Die ?.Invoke(...) in der Publish-Methode bedeutet, dass das Ereignis nur ausgelöst wird, wenn mindestens ein Abonnent registriert ist. Der Herausgeber löst das Ereignis aus, ohne zu wissen oder sich darum zu kümmern, ob jemand zuhört.

Siehe auch