Menu

C# Lambda-Ausdrücke: Syntax, Func und Action, Closures und LINQ

Wie Lambda-Ausdrücke in C# funktionieren: die =>-Syntax in allen Formen, Lambdas in Func und Action speichern, sie an Methoden und LINQ übergeben, Anweisungslambdas, Closures über erfasste Variablen und der Bug beim Erfassen in for-Schleifen, den foreach nicht hat.

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

Ein Lambda-Ausdruck ist eine kleine anonyme Funktion, die mit dem Operator => („geht über zu“) direkt im Code geschrieben wird. Die Parameter stehen links, das Ergebnis rechts:

Ausgabe:

60.0
12
ORDER SHIPPED!
96.0, 180.0

Ein Lambda hat keinen eigenen Typ. Es wird zu einem Delegate (einem Objekt, das auf eine Funktion zeigt) des Delegate-Typs, den der Kontext erwartet: hier Func<decimal, decimal>, ein Predicate<T> für List.FindAll, ein EventHandler für ein Event. Func-Delegates geben einen Wert zurück, Action-Delegates nicht; die Seite zu Func und Action listet die ganze Familie auf.

Syntaxformen

x => x * x                         // one parameter: parentheses optional
(x, y) => x + y                    // several parameters
() => DateTime.Now.Year            // no parameters
(int x, string s) => s.Length > x  // explicit parameter types
order =>                           // statement lambda: a block with return
{
    decimal subtotal = order.Qty * order.Price;
    return subtotal > 100m ? subtotal * 0.9m : subtotal;
}

Ein Ausdruckslambda hat nach => einen einzigen Ausdruck, und dessen Wert ist das Ergebnis. Ein Anweisungslambda hat einen Block in geschweiften Klammern, der beliebige Anweisungen enthalten kann und auf jedem Pfad mit return einen Wert zurückgeben muss, wenn der Delegate einen Wert zurückgibt (sonst CS1643, Not all code paths return a value in lambda expression).

Neuere Versionen haben weitere Formen hinzugefügt, alle optional:

  • C# 9: Discard-Parameter, sodass mehrere ungenutzte Parameter alle _ sein können ((_, _) => Save()), und static-Lambdas, denen das Erfassen von Variablen verboten ist.
  • C# 10: ein „natürlicher Typ“, sodass var square = (int x) => x * x; den Typ Func<int, int> ableitet. Davor scheitert var f = x => x * 2;; auch in C# 10 scheitert es ohne Parametertyp weiterhin, mit CS8917 The delegate type could not be inferred.
  • C# 12: Standardwerte für Parameter, (int x, int y = 1) => x + y.

Lambdas an Methoden übergeben

Jede Methode mit einem Delegate-Parameter akzeptiert ein Lambda. So übergibst du Verhalten, nicht nur Daten:

Ausgabe:

report.pdf, scan.pdf
photo.jpg, cv.txt, scan.pdf
cv.txt, scan.pdf, photo.jpg, report.pdf

Filter weiß nicht, was „behalten“ bedeutet; der Aufrufer entscheidet es mit einem Lambda. List.Sort funktioniert genauso: Das Lambda ist ein Vergleich, der einen negativen Wert, null oder einen positiven Wert zurückgibt, und Sort ruft es so oft auf, wie nötig.

Lambdas in LINQ

In LINQ begegnen die meisten C#-Programme Lambdas. Jeder Abfrageoperator nimmt eines: Where eine Bedingung, Select eine Projektion, OrderBy einen Schlüssel, Sum einen Wert zum Aufsummieren:

Ausgabe:

Ben: 250
Ana: 60
Cy: 45
Ana spent 80
Ben spent 250
Cy spent 45

Die Lambdas laufen nicht dort, wo sie geschrieben stehen. LINQ speichert sie und ruft sie auf, wenn die Abfrage durch foreach, ToList() oder Sum() durchlaufen wird, was verzögerte Ausführung heißt. Die LINQ-Seite behandelt die Operatoren selbst.

Closures: erfasste Variablen

Ein Lambda kann lokale Variablen und Parameter der Methode verwenden, in der es geschrieben ist. Es erfasst die Variable, nicht ihren aktuellen Wert, und die Variable lebt so lange weiter wie das Lambda:

Ausgabe:

1
2
3
1
50.0

Jeder Aufruf von MakeCounter erzeugt ein neues count, die beiden Zähler sind also unabhängig. Das Rabatt-Lambda hat rate gelesen, als es lief, nicht als es geschrieben wurde, und daher 50 % angewendet. Der Compiler setzt das um, indem er erfasste Variablen in eine versteckte Klasse verschiebt, die sich Methode und Lambda teilen.

Die Falle beim Erfassen in Schleifen

Beim Erfassen einer Schleifenvariable beißen Closures zu. Eine for-Schleife hat eine Variable für die ganze Schleife, jedes darin erzeugte Lambda teilt sich also diese Variable:

Ausgabe:

3 3 3 <- for loop, shared i
0 1 2 <- for loop, copied
0 1 2 <- foreach

Alle drei Lambdas der ersten Schleife liefen, nachdem die Schleife beendet war, als i 3 war. i in eine im Rumpf deklarierte Variable zu kopieren gibt jedem Lambda seine eigene. foreach macht es von selbst richtig: Seit C# 5 ist seine Iterationsvariable bei jedem Durchlauf eine neue Variable. Vor C# 5 hatte es denselben Bug, weshalb ältere Antworten im Netz die Kopie auch für foreach noch empfehlen.

Dieselbe Falle gilt für alles, was später läuft: Event-Handler, Timer, Task.Run und LINQ-Abfragen, die in einer Schleife gebaut und danach durchlaufen werden.

Lambdas, anonyme Methoden und lokale Funktionen

Lambdas haben die Syntax der anonymen Methoden aus C# 2 abgelöst, die aber weiterhin kompiliert:

Func<int, bool> isEven = delegate (int n) { return n % 2 == 0; };   // C# 2 anonymous method
Func<int, bool> isEven2 = n => n % 2 == 0;                          // lambda

Für eine Hilfsfunktion, die nur in einer Methode verwendet wird, bietet C# 7.0 außerdem lokale Funktionen, die einen Namen haben, rekursiv sein können und keinen Delegate allokieren, solange du sie nicht in einen umwandelst. Nimm ein Lambda, wenn du Verhalten an etwas übergibst (LINQ, Sort, ein Event); nimm eine lokale Funktion oder eine private Methode, wenn du sie selbst aufrufst.

Ein weiterer Unterschied zeigt sich bei LINQ-Providern wie Entity Framework: Ein Lambda, das einem Expression<Func<T, bool>> zugewiesen wird, wird gar nicht zu Code kompiliert, sondern zu einer Datenstruktur, die den Code beschreibt und die der Provider in SQL übersetzt. Die Syntax ist identisch; der Parametertyp der aufgerufenen Methode entscheidet, was du bekommst.

Häufig gestellte Fragen

Was ist ein Lambda-Ausdruck in C#?

Eine anonyme Funktion, geschrieben mit dem Operator =>: x => x * 2 nimmt x und gibt x * 2 zurück. Ein Lambda hat keinen eigenen Namen; es wird in einen Delegate-Typ wie Func<int, int> oder Action<string> umgewandelt und kann in einer Variable gespeichert, an eine Methode übergeben oder aus einer zurückgegeben werden.

Wie schreibe ich ein Lambda mit mehreren Parametern oder ohne Parameter?

Setze die Parameter in Klammern: (a, b) => a + b für zwei, () => DateTime.Now für keinen. Nur ein einzelner Parameter darf ohne Klammern stehen (x => x + 1). Du kannst die Typen auch explizit angeben: (int a, int b) => a + b.

Was ist eine Closure bei einem C#-Lambda?

Ein Lambda, das eine lokale Variable der umgebenden Methode verwendet, erfasst diese Variable, keine Momentaufnahme ihres Werts. Die Variable lebt so lange wie das Lambda, und wenn eine Seite sie später ändert, sieht die andere die Änderung. Das lässt Zähler und Callbacks funktionieren und verursacht den klassischen Bug beim Erfassen in for-Schleifen.

Warum sehen Lambdas in einer for-Schleife alle denselben Wert?

Eine for-Schleife hat eine einzige Schleifenvariable für die ganze Schleife, jedes darin erzeugte Lambda erfasst also dasselbe i und sieht seinen Endwert, wenn es später läuft. Kopiere es im Rumpf in eine lokale Variable (int copy = i;) und erfasse die Kopie. foreach hat dieses Problem seit C# 5 nicht, weil es pro Durchlauf eine neue Variable erzeugt.

Warum kompiliert var f = x => x * 2 nicht?

Der Compiler kann nicht erkennen, welchen Delegate-Typ du willst oder welchen Typ x hat, daher meldet er CS8917 The delegate type could not be inferred (vor C# 10 CS0815). Deklariere den Typ (Func<int, int> f = x => x * 2;) oder, ab C# 10, typisiere den Parameter: var f = (int x) => x * 2;.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S