Lambdaausdrücke (C#-Referenz)

Sie verwenden einen Lambdaausdruck, um eine anonyme Funktion zu erstellen. Verwenden Sie den Lambdadeklarationsoperator =>, um die Parameterliste des Lambdas von dessen Text zu trennen. Ein Lambdaausdruck kann eine der folgenden beiden Formen aufweisen:

  • Ein Ausdruckslambda mit einem Ausdruck als Text:

    (input-parameters) => expression
    
  • Ein Anweisungslambda mit einem Anweisungsblock als Text:

    (input-parameters) => { <sequence-of-statements> }
    

Geben Sie zum Erstellen eines Lambdaausdrucks Eingabeparameter (falls vorhanden) auf der linken Seite des Lambdaoperators und einen Ausdruck oder Anweisungsblock auf der anderen Seite an.

Jeder Lambdaausdruck kann in einen Delegat-Typ konvertiert werden. Der Delegattyp, in den ein Lambdaausdruck konvertiert werden kann, wird durch die Typen seiner Parameter und Rückgabewerte definiert. Wenn ein Lambdaausdruck keinen Wert zurückgibt, kann er in einen der Action-Delegattypen konvertiert werden. Andernfalls kann er in einen der Func-Delegattypen konvertiert werden. Ein Lambdaausdruck, der beispielsweise zwei Parameter hat und keinen Wert zurückgibt, kann in einen Action<T1,T2>-Delegat konvertiert werden. Außerdem kann ein Lambdaausdruck, der einen Parameter hat und einen Wert zurückgibt, in einen Func<T,TResult>-Delegat konvertiert werden. Im folgenden Beispiel wird der Lambdaausdruck x => x * x, der einen Parameter x angibt und den Wert von x im Quadrat zurückgibt, einer Variablen eines Delegattyps zugewiesen:

Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
// Output:
// 25

Zudem lassen sich Ausdruckslambdas in Ausdrucksbaumstruktur-Typen konvertieren, wie im folgenden Beispiel gezeigt:

System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// Output:
// x => (x * x)

Sie können Lambdaausdrücke in jedem Code verwenden, der Instanzen von Delegattypen oder Ausdrucksbaumstrukturen erfordert, z.B. als Argument für die Task.Run(Action)-Methode, um den im Hintergrund auszuführenden Code zu übergeben. Wie im folgenden Beispiel gezeigt wird, können Sie beim Schreiben von LINQ in C# auch Lambdaausdrücke verwenden:

int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// Output:
// 4 9 16 25

Wenn Sie zum Aufrufen der Enumerable.Select-Methode in der System.Linq.Enumerable-Klasse methodenbasierte Syntax verwenden, wie in LINQ to Objects und LINQ to XML, ist der Parameter ein Delegattyp System.Func<T,TResult>. Wenn Sie die Queryable.Select-Methode in der System.Linq.Queryable-Klasse aufrufen, wie in LINQ to SQL, ist der Parametertyp ein Ausdrucksbaumstruktur-Typ Expression<Func<TSource,TResult>>. In beiden Fällen können Sie denselben Lambdaausdruck verwenden, um die Parameterwerte anzugeben. Dadurch sehen die Select-Aufrufe ähnlich aus, obwohl sich der mithilfe der Lambdas erstellte Objekttyp unterscheidet.

Ausdruckslambdas

Ein Lambdaausdruck mit einem Ausdruck auf der rechten Seite des =>-Operators wird als Ausdruckslambda bezeichnet. Ein Ausdruckslambda gibt das Ergebnis des Ausdrucks zurück und hat folgende grundlegende Form:

(input-parameters) => expression

Der Text eines Ausdruckslambdas kann aus einem Methodenaufruf bestehen. Wenn Sie jedoch Ausdrucksbaumstrukturen erstellen, die außerhalb des Kontexts der .NET Common Language Runtime (CLR) ausgewertet werden, z. B. in SQL Server, sollten Sie in Lambdaausdrücken keine Methodenaufrufe verwenden. Die Methoden haben außerhalb des Kontexts der .NET Common Language Runtime (CLR) keine Bedeutung.

Anweisungslambdas

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

(input-parameters) => { <sequence-of-statements> }

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

Action<string> greet = name =>
{
    string greeting = $"Hello {name}!";
    Console.WriteLine(greeting);
};
greet("World");
// Output:
// Hello World!

Anweisungslambdas können nicht zum Erstellen von Ausdrucksbaumstrukturen verwendet werden.

Eingabeparameter eines Lambdaausdrucks

Sie schließen die Eingabeparameter eines Lambdaausdrucks in Klammern ein. Geben Sie Eingabeparameter von 0 (null) mit leeren Klammern an:

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

Wenn ein Lambdaausdruck nur über einen Eingabeparameter verfügt, sind Klammern optional:

Func<double, double> cube = x => x * x * x;

Zwei oder mehr Eingabeparameter werden durch Kommas getrennt:

Func<int, int, bool> testForEquality = (x, y) => x == y;

Manchmal kann der Compiler die Typen von Eingabeparametern nicht ableiten. Sie können die Typen wie im folgenden Beispiel dargestellt explizit angeben:

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

Eingabeparametertypen müssen alle entweder explizit oder implizit sein. Andernfalls tritt der Compilerfehler CS0748 auf.

Ab C# 9.0 können Sie discards verwenden, um mindestens zwei Eingabeparameter eines Lambdaausdrucks anzugeben, der nicht im Ausdruck verwendet wird:

Func<int, int, int> constant = (_, _) => 42;

Verwerfungsparameter von Lambdas können nützlich sein, wenn Sie einen Lambdaausdruck zur Bereitstellung eines Ereignishandlers verwenden.

Hinweis

Aus Gründen der Abwärtskompatibilität wird innerhalb eines Lambdaausdrucks _ als Name des Parameters behandelt, wenn nur ein einzelner Eingabeparameter den Namen _ aufweist.

Asynchrone Lambdas

Sie können mit den Schlüsselwörtern async und await Lambda-Ausdrücke und Anweisungen, die asynchrone Verarbeitung enthalten, leicht erstellen. Das folgende Windows Forms enthält z. B. einen Ereignishandler, der eine Async-Methode, ExampleMethodAsync, aufruft und erwartet.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        button1.Click += button1_Click;
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        await ExampleMethodAsync();
        textBox1.Text += "\r\nControl returned to Click event handler.\n";
    }

    private async Task ExampleMethodAsync()
    {
        // The following line simulates a task-returning asynchronous process.
        await Task.Delay(1000);
    }
}

Sie können denselben Ereignishandler hinzufügen, indem Sie ein asynchrones Lambda verwenden. Um diesen Handler hinzuzufügen, fügen Sie einen async-Modifizierer vor der Lambdaparameterliste hinzu, wie im folgenden Beispiel dargestellt:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        button1.Click += async (sender, e) =>
        {
            await ExampleMethodAsync();
            textBox1.Text += "\r\nControl returned to Click event handler.\n";
        };
    }

    private async Task ExampleMethodAsync()
    {
        // The following line simulates a task-returning asynchronous process.
        await Task.Delay(1000);
    }
}

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 integrierte Unterstützung 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 drei Komponenten verwenden, um eine Zahlensequenz an einen Lambdaausdruck zu übergeben; dadurch wird jeder Wert verdoppelt, und es wird ein Tupel mit drei Komponenten zurückgegeben, das das Ergebnis der Multiplikation enthält.

Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
// Output:
// The set (2, 3, 4) doubled: (4, 6, 8)

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.

Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");

Weitere Informationen zu C#-Tupeln finden Sie unter Tupeltypen.

Lambdas mit Standardabfrageoperatoren

LINQ to Objects haben, neben anderen Implementierungen, einen Eingabeparameter, dessen Typ Teil der Func<TResult>-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. Betrachten Sie z.B. den Func<T,TResult>-Delegattyp:

public delegate TResult Func<in T, out TResult>(T arg)

Der Delegat kann als Func<int, bool>-Instanz instanziiert werden, wobei int ein Eingabeparameter und bool der Rückgabewert ist. Der Rückgabewert wird immer im letzten Typparameter angegeben. Func<int, string, bool> definiert z.B. einen Delegaten mit zwei Eingabeparametern, int und string, und einen Rückgabetyp von bool. Der folgende Func-Delegat gibt bei einem Aufruf einen booleschen Wert zurück, um anzugeben, ob der Eingabeparameter gleich fünf ist:

Func<int, bool> equalsFive = x => x == 5;
bool result = equalsFive(4);
Console.WriteLine(result);   // False

Sie können einen Lambdaausdruck auch dann angeben, wenn der Argumenttyp Expression<TDelegate> ist, beispielsweise in den Standardabfrageoperatoren, die in Typ Queryable definiert sind. Wenn Sie ein Expression<TDelegate>-Argument angeben, wird der Lambdaausdruck in eine Ausdrucksbaumstruktur kompiliert.

Im folgenden Beispiel wird der Count-Standardabfrageoperator 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 {oddNumbers} odd numbers in {string.Join(" ", numbers)}");

Der Compiler kann den Typ des Eingabeparameters ableiten, Sie können ihn aber auch explizit angeben. Dieser bestimmte Lambda-Ausdruck zählt die ganzen Zahlen (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:

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstNumbersLessThanSix = numbers.TakeWhile(n => n < 6);
Console.WriteLine(string.Join(" ", firstNumbersLessThanSix));
// 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 numbers-Array zurückgegeben, bis eine Zahl erreicht wird, deren Wert kleiner ist als ihre Ordnungspostion im Array:

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);
Console.WriteLine(string.Join(" ", firstSmallNumbers));
// Output:
// 5 4

Sie verwenden Lambdaausdrücke nicht direkt in Abfrageausdrücken, aber Sie können sie in Methodenaufrufen innerhalb von Abfrageausdrücken verwenden, wie das folgende Beispiel zeigt:

var numberSets = new List<int[]>
{
    new[] { 1, 2, 3, 4, 5 },
    new[] { 0, 0, 0 },
    new[] { 9, 8 },
    new[] { 1, 0, 1, 0, 1, 0, 1, 0 }
};

var setsWithManyPositives = 
    from numberSet in numberSets
    where numberSet.Count(n => n > 0) > 3
    select numberSet;

foreach (var numberSet in setsWithManyPositives)
{
    Console.WriteLine(string.Join(" ", numberSet));
}
// Output:
// 1 2 3 4 5
// 1 0 1 0 1 0 1 0

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, der Parametertypen und anderer Faktoren 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.

  • Jeder Eingabeparameter im Lambda muss implizit in den entsprechenden Delegatparameter konvertiert werden können.

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

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

Erfassen äußerer Variablen sowie des Variablenbereichs in Lambdaausdrücken

Lambdas können auf äußere Variablen verweisen. Hierbei handelt es sich um die Variablen, die im Bereich der Methode, mit der der Lambdaausdruck 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:

public static class VariableScopeWithLambdas
{
    public class VariableCaptureGame
    {
        internal Action<int> updateCapturedLocalVariable;
        internal Func<int, bool> isEqualToCapturedLocalVariable;

        public void Run(int input)
        {
            int j = 0;

            updateCapturedLocalVariable = x =>
            {
                j = x;
                bool result = j > input;
                Console.WriteLine($"{j} is greater than {input}: {result}");
            };

            isEqualToCapturedLocalVariable = x => x == j;

            Console.WriteLine($"Local variable before lambda invocation: {j}");
            updateCapturedLocalVariable(10);
            Console.WriteLine($"Local variable after lambda invocation: {j}");
        }
    }

    public static void Main()
    {
        var game = new VariableCaptureGame();

        int gameInput = 5;
        game.Run(gameInput);

        int jTry = 10;
        bool result = game.isEqualToCapturedLocalVariable(jTry);
        Console.WriteLine($"Captured local variable is equal to {jTry}: {result}");

        int anotherJ = 3;
        game.updateCapturedLocalVariable(anotherJ);

        bool equalToAnother = game.isEqualToCapturedLocalVariable(anotherJ);
        Console.WriteLine($"Another lambda observes a new value of captured variable: {equalToAnother}");
    }
    // Output:
    // Local variable before lambda invocation: 0
    // 10 is greater than 5: True
    // Local variable after lambda invocation: 10
    // Captured local variable is equal to 10: True
    // 3 is greater than 5: False
    // Another lambda observes a new value of captured variable: 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 Lambdaausdruck eingeführt wurden, sind in der einschließenden Methode nicht sichtbar.

  • Ein Lambdaausdruck kann einen in-, ref- oder out-Parameter nicht direkt von der einschließenden Methode erfassen.

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

  • Ein Lambdaausdruck darf keine goto-, break- oder continue-Anweisung enthalten, wenn das Ziel dieser Sprunganweisung außerhalb des Lambdaausdrucksblocks liegt. Eine Sprunganweisung darf auch nicht außerhalb des Lambdaausdrucksblocks sein, wenn das Ziel im Block ist.

Ab C# 9.0 können Sie den static-Modifizierer auf einen Lambdaausdruck anwenden, um zu verhindern, dass lokale Variablen oder der Instanzzustand versehentlich durch die Lambdafunktion erfasst werden:

Func<double, double> square = static x => x * x;

Ein statischer Lambdaausdruck kann keine lokalen Variablen oder den Instanzzustand aus einschließenden Bereichen erfassen, kann jedoch auf statische Member und Konstantendefinitionen verweisen.

C#-Sprachspezifikation

Weitere Informationen finden Sie im Abschnitt Anonyme Funktionsausdrücke der C#-Sprachspezifikation.

Weitere Informationen zu in C# 9.0 eingeführten Features finden Sie in den folgenden Featurevorschlägen:

Weitere Informationen