Lambdaausdrücke

Ein Lambdaausdruck ist ein Codeblock (bzw. ein Ausdrucks- oder ein Anweisungsblock), der wie ein Objekt behandelt wird. Er kann als Argument an eine Methode übergeben werden, und er kann auch von Methodenaufrufen zurückgegeben werden. Lambdaausdrücke finden bei folgenden Aktionen Anwendung:

Lambdaausdrücke sind Code, der entweder als Delegat oder als eine Ausdrucksbaumstruktur repräsentiert werden kann, die an einen Delegat kompiliert. Der genaue Delegattyp eines Lambdaausdrucks hängt von dessen Parametern und Rückgabewert ab. Lambdaausdrücke, die keinen Wert zurückgeben, korrespondieren mit einem bestimmten Action-Delegat, abhängig von der Anzahl seiner Parameter. Lambdaausdrücke, die keinen Wert zurückgeben, entsprechen einem bestimmten Func-Delegat, abhängig von der Anzahl seiner Parameter. Ein Lambdaausdruck, der beispielsweise zwei Parameter hat, aber keinen Wert zurück gibt, entspricht einem @System.Action%602-Delegat. Ein Lambdaausdruck, der beispielsweise zwei Parameter hat, aber keinen Wert zurück gibt, entspricht einem @System.Func%602-Delegat.

Ein Lambdaausdruck verwendet =>, den Lambdadeklarationsoperator, um die Parameterliste des Lambdas von dessen ausführbarem Code zu trennen. Zum Erstellen eines Lambdaausdrucks geben Sie Eingabeparameter (falls vorhanden) auf der linken Seite des Lambdaoperators an und stellen den Ausdrucks- oder Anweisungsblock auf die andere Seite. Beispielsweise gibt der Einzelzeilen-Lambdaausdruck x => x * x einen Parameter an, der x heißt und das Quadrat des Werts von x zurückgibt. Dieser Ausdruck kann einem Delegattyp zuwiesen werden, wie im folgenden Beispiel dargestellt:

using System;

class Example
{
   public static void Main()
   {
      Func<int, int> square = x => x * x; 
      Console.WriteLine(square(25));
   }
}
// The example displays the following output:
//      625

Sie können ihn aber auch als Methodenargument übergeben:

using System;

public class Example
{
   static void Main()  
   {  
      ShowValue(x => x * x);  
   }  

   private static void ShowValue(Func<int,int> op)
   {
      for (int ctr = 1; ctr <= 5; ctr++)
         Console.WriteLine("{0} x {0} = {1}",
                           ctr,  op(ctr));
   }
}
// The example displays the following output:
//   1, 1 x 1 = 1
//   2, 2 x 2 = 4
//   3, 3 x 3 = 9
//   4, 4 x 4 = 16
//   5, 5 x 5 = 25

Ausdruckslambdas

Ein Lambdaausdruck mit einem Ausdruck auf der rechten Seite des Operators „=>“ wird als Ausdruckslambda bezeichnet. Ausdruckslambdas werden häufig bei der Erstellung von Ausdrucksbaumstrukturen verwendet. Ein Ausdruckslambda gibt das Ergebnis des Ausdrucks zurück und hat folgende grundlegende Form:

(input parameters) => expression

Die Klammern sind nur optional, wenn das Lambda über einen Eingabeparameter verfügt; andernfalls sind sie erforderlich. Geben Sie Eingabeparameter von 0 (null) mit leeren Klammern an:

Action line = () => Console.WriteLine();

Zwei oder mehr Eingabeparameter sind durch Kommas getrennt und in Klammern eingeschlossen:

Func<int,int,bool> testEquality = (x, y) => x == y;  // test for equality

Für gewöhnlich verwendet der Compiler den Typrückschluss, um Parametertypen zu ermitteln. Für den Compiler kann es jedoch schwierig oder unmöglich sein, die Eingabetypen abzuleiten. Wenn dieses Problem auftritt, können Sie die Typen explizit angeben, wie im folgenden Beispiel dargestellt:

Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;

Beachten Sie im vorherigen Beispiel, dass der Text eines Ausdruckslambdas ein Methodenaufruf sein kann. Wenn Sie allerdings Ausdrucksbaumstrukturen erstellen, die außerhalb des .NET Frameworks ausgewertet werden, wie z.B. in SQL Server oder Entity Framework (EF), sollten Sie davon absehen, Methodenaufrufe in Lambdaausdrücken zu verwenden, da die Methoden möglicherweise außerhalb des Kontexts der .NET-Implementierung bedeutungslos sind. Falls Sie dennoch Methodenaufrufe verwenden möchten, prüfen Sie diese gründlich, um sicherzustellen, dass die Methodenaufrufe erfolgreich aufgelöst werden können.

Anweisungslambdas

Ein Anweisungslambda ähnelt einem Ausdruckslambda, allerdings sind die Anweisungen in Klammern eingeschlossen:

(input parameters) => { statement; }

Der Text eines Anweisungslambdas kann aus einer beliebigen Anzahl von Anweisungen bestehen, wobei es sich meistens um höchstens zwei oder drei Anweisungen handelt.

using System;


public class Example
{
    delegate void TestDelegate(string s);

    public static void Main()
    {
       TestDelegate test = n => { string s = n + " " + "World"; Console.WriteLine(s); };  
       test("Hello");
    }
}
// The example displays the following output:
//     Hello World

Anweisungslambdas, wie anonyme Methoden, können nicht zum Erstellen von Ausdrucksbaumstrukturen verwendet werden.

Asynchrone Lambdas

Sie können mit den Schlüsselwörtern async und await Lambdaausdrücke und Anweisungen, die asynchrone Verarbeitung enthalten, leicht erstellen. Das Beispiel ruft beispielsweise eine ShowSquares-Methode auf, die asynchron ausgeführt wird.

using System;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      Begin().Wait();
   }

   private static async Task Begin()
   {
      for (int ctr = 2; ctr <= 5; ctr++) {
         var result = await ShowSquares(ctr);
         Console.WriteLine("{0} * {0} = {1}", ctr, result);
      }
   }

   private static async Task<int>  ShowSquares(int number)
   {
         return await Task.Factory.StartNew( x => (int)x * (int)x, number);
   } 
}

Weitere Informationen zum Erstellen und Verwenden von asynchronen Methoden finden Sie unter Asynchronous programming with async and await (Asynchrones Programmieren mit „async“ and „await“).

Lambdaausdrücke und Tupel

Ab C# 7.0 bietet die C#-Programmiersprache integrierten Support für Tupel. Sie können ein Tupel einem Lambdaausdruck als Argument bereitstellen, und Ihr Lambdaausdruck kann ebenfalls einen Tupel zurückgeben. In einigen Fällen verwendet der C#-Compiler den Typrückschluss, um den Typ der Tupelkomponenten zu ermitteln.

Sie können ein Tupel definieren, indem Sie eine durch Trennzeichen getrennte Liste seiner Komponenten in Klammern einschließen. In folgendem Beispiel wird ein Tupel mit fünf Komponenten verwenden, um eine Zahlensequenz an einen Lambdaausdruck zu übergeben; dadurch wird jeder Wert verdoppelt, und es wird ein Tupel mit fünf Komponenten zurückgegeben, das das Ergebnis der Multiplikation enthält.

using System;

public class Example
{
    public static void Main()
    {
        var numbers = (2, 3, 4, 5, 6);
        Func<(int, int, int, int, int), (int, int, int, int, int)> doubleThem = (n) => (n.Item1 * 2, n.Item2 * 2, n.Item3 * 2, n.Item4 * 2, n.Item5 * 2);
        var doubledNumbers = doubleThem(numbers);

        Console.WriteLine("The set {0} doubled: {1}", numbers, doubledNumbers);
        Console.ReadLine();
    }
}
// The example displays the following output:
//    The set (2, 3, 4, 5, 6) doubled: (4, 6, 8, 10, 12)

Für gewöhnlich heißen die Felder eines Tupels Item1, Item2, usw. Sie können allerdings ein Tupel mit benannten Komponenten definieren, wie in folgendem Beispiel veranschaulicht.

using System;

public class Example
{
    public static void Main()
    {
        var numbers = (2, 3, 4, 5, 6);
        Func<(int n1, int n2, int n3, int n4, int n5), (int, int, int, int, int)> doubleThem = (n) => (n.n1 * 2, n2 * 2, n.n3 * 2, n.n4 * 2, n.n5 * 2);
        var doubledNumbers = doubleThem(numbers);

        Console.WriteLine("The set {0} doubled: {1}", numbers, doubledNumbers);
        Console.ReadLine();
    }
}
// The example displays the following output:
//    The set (2, 3, 4, 5, 6) doubled: (4, 6, 8, 10, 12)

Weitere Informationen zum Support für Tupel in C# finden Sie unter C# Tuple types (C#-Tupeltypen).

Lambdas mit Standardabfrageoperatoren

LINQ to Objects haben, neben anderen Implementierungen, einen Eingabeparameter, dessen Typ Teil der @System.Func%601-Familie generischer Delegate ist. Diese Delegaten verwenden Typparameter zur Definition der Anzahl und des Typs der Eingabeparameter sowie des Rückgabetyps des Delegaten. Func-Delegaten sind für das Kapseln von benutzerdefinierten Ausdrücken, die für jedes Element in einem Satz von Quelldaten übernommen werden, sehr nützlich. Berücksichtigen z.B. den @System.Func%601-Delegaten, dessen Syntax wie folgt lautet:

public delegate TResult Func<TArg, TResult>(TArg arg);

Der Delegat kann mit Code wie dem folgenden instanziiert werden

Func<int, bool> func = (x) => x == 5; 

Hier ist int ein Eingabeparameter, und bool ist der Rückgabewert. Der Rückgabewert wird immer im letzten Typparameter angegeben. Wenn der folgende Func-Delegat aufgerufen wird, gibt er TRUE oder FALSE zurück, um anzugeben, ob der Eingabeparameter gleich 5 ist:

Console.WriteLine(func(4));      // Returns "False".     

Sie können einen Lambdaausdruck auch dann angeben, wenn der Argumenttyp @System.Linq.Expressions.Expression&601 ist, beispielsweise in den Standardabfrageoperatoren, die im Typ Queryable definiert sind. Wenn Sie ein @System.Linq.Expressions.Expression%601-Argument angeben, wird der Lambdaausdruck in eine Ausdrucksbaumstruktur kompiliert. In folgendem Beispiel wird der Standardabfrageoperator System.Linq.Enumerable.Count verwendet.

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };  
int oddNumbers = numbers.Count(n => n % 2 == 1);  
Console.WriteLine("There are {0} odd numbers in the set", oddNumbers);
// Output: There are 5 odd numbers in the set

Der Compiler kann den Typ des Eingabeparameters ableiten, Sie können ihn aber auch explizit angeben. Dieser bestimmte Lambdaausdruck zählt die Ganzzahlen (n), bei denen nach dem Dividieren durch zwei als Rest 1 bleibt.

In folgendem Beispiel wird eine Sequenz erzeugt, die alle Elemente im Array numbers enthält, die vor der 9 auftreten, da dies die erste Zahl in der Sequenz ist, die die Bedingung nicht erfüllt.

var firstNumbersLessThan6 = numbers.TakeWhile(n => n < 6);
foreach (var number in firstNumbersLessThan6)
   Console.Write("{0}     ", number);  
// Output: 5     4     1     3

In folgendem Beispiel wird gezeigt, wie Sie mehrere Eingabeparameter angeben, indem Sie sie in Klammern einschließen. Mit der Methode werden alle Elemente im Zahlenarray zurückgegeben, bis eine Zahl erreicht wird, deren Wert kleiner ist als seine Ordnungspostion im Array.

 var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);
foreach (var number in firstSmallNumbers)
   Console.Write("{0}     ", number);
 // Output: 5     4

Typrückschluss in Lambdaausdrücken

Beim Schreiben von Lambdas müssen Sie oftmals keinen Typ für die Eingabeparameter angeben, da der Compiler den Typ auf der Grundlage des Lambdatexts, des zugrunde liegenden Typs des Parameters und anderer Faktoren per Rückschluss ableiten kann, wie in der C#-Programmiersprachenspezifikation beschrieben. Bei den meisten Standardabfrageoperatoren entspricht die erste Eingabe dem Typ der Elemente in der Quellsequenz. Beim Abfragen von IEnumerable<Customer> wird die Eingabevariable als Customer-Objekt abgeleitet, sodass Sie auf die zugehörigen Methoden und Eigenschaften zugreifen können:

customers.Where(c => c.City == "London");

Die allgemeinen Regeln für Typrückschlüsse bei Lambdas lauten wie folgt:

  • Der Lambda-Ausdruck muss dieselbe Anzahl von Parametern enthalten wie der Delegattyp.

  • Jedes Eingabeargument im Lambda muss implizit in den entsprechenden Delegatparameter konvertierbar sein.

  • Der Rückgabewert des Lambdas (falls vorhanden) muss implizit in den Rückgabetyp des Delegaten konvertiert werden können.

Beachten Sie, dass Lambda-Ausdrücke keinen eigenen Typ haben, da das allgemeine Typsystem kein internes Konzept von "Lambda-Ausdrücken" aufweist. Es kann manchmal praktisch sein, informell vom "Typ" eines Lambda-Ausdrucks zu sprechen. In einem solchen Fall bezeichnet Typ den Delegattyp bzw. den Expression -Typ, in den der Lambda-Ausdruck konvertiert wird.

Variablenbereich in Lambda-Ausdrücken

Lambdas können auf äußere Variablen verweisen (siehe Anonyme Methoden), die im Bereich der Methode, mit der die Lambdafunktion definiert wird, oder im Bereich des Typs liegen, der den Lambdaausdruck enthält. Variablen, die auf diese Weise erfasst werden, werden zur Verwendung in Lambda-Ausdrücken gespeichert, auch wenn die Variablen andernfalls außerhalb des Gültigkeitsbereichs liegen und an die Garbage Collection übergeben würden. Eine äußere Variable muss definitiv zugewiesen sein, bevor sie in einem Lambda-Ausdruck verwendet werden kann. Das folgende Beispiel veranschaulicht diese Regeln.

using System;

delegate bool D();  
delegate bool D2(int i);  
  
class Test  
{  
    D del;  
    D2 del2;  
    public void TestMethod(int input)  
    {  
        int j = 0;  
        // Initialize the delegates with lambda expressions.  
        // Note access to 2 outer variables.  
        // del will be invoked within this method.  
        del = () => { j = 10;  return j > input; };  
  
        // del2 will be invoked after TestMethod goes out of scope.  
        del2 = (x) => {return x == j; };  
  
        // Demonstrate value of j:  
        // Output: j = 0   
        // The delegate has not been invoked yet.  
        Console.WriteLine("j = {0}", j);        // Invoke the delegate.  
        bool boolResult = del();  
  
        // Output: j = 10 b = True  
        Console.WriteLine("j = {0}. b = {1}", j, boolResult);  
    }  
  
    static void Main()  
    {  
        Test test = new Test();  
        test.TestMethod(5);  
  
        // Prove that del2 still has a copy of  
        // local variable j from TestMethod.  
        bool result = test.del2(10);  
  
        // Output: True  
        Console.WriteLine(result);  
    }  
}  
// The example displays the following output:
//      j = 0
//      j = 10. b = True
//      True

Die folgenden Regeln gelten für den Variablenbereich in Lambda-Ausdrücken:

  • Eine erfasste Variable wird erst dann an die Garbage Collection übergeben, wenn der darauf verweisende Delegat für die Garbage Collection geeignet ist.

  • Variablen, die in einem Lambda-Ausdruck eingeführt wurden, sind in der äußeren Methode nicht sichtbar.

  • Ein Lambda-Ausdruck kann einen ref - oder out -Parameter nicht direkt von einer einschließenden Methode erfassen.

  • Eine return-Anweisung in einem Lambda-Ausdruck bewirkt keine Rückgabe durch die einschließende Methode.

  • Ein Lambda-Ausdruck kann eine goto -Anweisung, break -Anweisung oder continue -Anweisung enthalten, die innerhalb der Lambda-Funktion liegt, wenn das Ziel der jump-Anweisung außerhalb des Blocks liegt. Eine jump-Anweisung darf auch nicht außerhalb des Lambda-Funktionsblocks sein, wenn das Ziel im Block ist.

Siehe auch

LINQ (Language Integrated Query)
Anonyme Methoden
Ausdrucksbaumstrukturen