Udostępnij za pośrednictwem


Wyrażenia lambda i funkcje anonimowe

Aby utworzyć funkcję anonimową, należy użyć wyrażenia lambda . Użyj operatora deklaracji lambda =>, aby oddzielić listę parametrów lambdy od jej treści. Wyrażenie lambda może mieć dowolną z następujących dwóch form:

  • wyrażenie lambda, które ma wyrażenie jako treść:

    (input-parameters) => expression
    
  • Instrukcja lambda, która ma blok instrukcji jako jego treść:

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

Aby utworzyć wyrażenie lambda, należy określić parametry wejściowe (jeśli istnieją) po lewej stronie operatora lambda i wyrażenie lub blok instrukcji po drugiej stronie.

Dowolne wyrażenie lambda można przekonwertować na delegata typu. Typy jego parametrów i wartość zwracana definiują typ delegata, do którego można przekonwertować wyrażenie lambda. Jeśli wyrażenie lambda nie zwraca wartości, można ją przekonwertować na jeden z typów delegatów Action; w przeciwnym razie można go przekonwertować na jeden z Func typów delegatów. Na przykład wyrażenie lambda, które ma dwa parametry i nie zwraca żadnej wartości, można przekonwertować na delegata Action<T1,T2>. Wyrażenie lambda, które ma jeden parametr i zwraca wartość, można przekonwertować na Func<T,TResult> delegata. W poniższym przykładzie wyrażenie lambda x => x * x, które określa parametr o nazwie x i zwraca wartość x kwadratu, jest przypisywany do zmiennej typu delegata:

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

Wyrażenia lambda można również przekonwertować na typy drzew wyrażeń , jak pokazano w poniższym przykładzie:

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

Wyrażenia lambda są używane w dowolnym kodzie, który wymaga wystąpień typów delegatów lub drzew wyrażeń. Jednym z przykładów jest argument metody Task.Run(Action) do przekazania kodu, który powinien zostać wykonany w tle. Wyrażenia lambda można również użyć podczas pisania LINQ w języku C#, jak pokazano w poniższym przykładzie:

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

Jeśli używasz składni opartej na metodzie do wywoływania metody Enumerable.Select w klasie System.Linq.Enumerable, na przykład w linQ to Objects i LINQ to XML, parametr jest typem delegata System.Func<T,TResult>. Podczas wywoływania metody Queryable.Select w klasie System.Linq.Queryable, na przykład w LINQ to SQL, parametr ma typ drzewa wyrażeń Expression<Func<TSource,TResult>>. W obu przypadkach można użyć tego samego wyrażenia lambda, aby określić wartość parametru. To sprawia, że dwa wywołania Select są do siebie podobne, chociaż w rzeczywistości typ obiektów tworzonych za pomocą wyrażeń lambda jest inny.

Wyrażenia lambda

Wyrażenie lambda, które ma wyrażenie po prawej stronie operatora =>, nazywa się wyrażeniem lambda . Wyrażenie lambda zwraca wynik wyrażenia i przyjmuje następującą podstawową formę:

(input-parameters) => expression

Treść wyrażenia lambda może składać się z wywołania metody. Jeśli jednak tworzysz drzewa wyrażeń , które są oceniane poza kontekstem środowiska uruchomieniowego języka wspólnego platformy .NET (CLR), na przykład w programie SQL Server, nie należy używać wywołań metod w wyrażeniach lambda. Metody nie mają znaczenia poza kontekstem środowiska uruchomieniowego języka wspólnego platformy .NET (CLR).

Wyrażenia lambda

Instrukcja typu lambda przypomina wyrażenie lambda z tą różnicą, że jej instrukcje są ujęte w nawiasy klamrowe.

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

Treść instrukcji lambda może składać się z dowolnej liczby instrukcji; jednak w praktyce zwykle nie ma więcej niż dwóch lub trzech.

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

Nie można używać lambd instrukcji do tworzenia drzew wyrażeń.

Parametry wejściowe wyrażenia lambda

Parametry wejściowe wyrażenia lambda są ujęte w nawiasy. Określ zero parametrów wejściowych z pustymi nawiasami:

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

Jeśli wyrażenie lambda ma tylko jeden parametr wejściowy, nawiasy są opcjonalne:

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

Dwa lub więcej parametrów wejściowych jest rozdzielonych przecinkami:

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

Czasami kompilator nie może wywnioskować typów parametrów wejściowych. Typy można określić jawnie, jak pokazano w poniższym przykładzie:

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

Typy parametrów wejściowych muszą być jawne lub wszystkie niejawne; w przeciwnym razie wystąpi błąd kompilatora CS0748.

Za pomocą można pomijać, aby określić dwa lub więcej parametrów wejściowych wyrażenia lambda, które nie są używane w wyrażeniu:

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

Parametry odrzucenia lambda mogą być przydatne, gdy używasz wyrażenia lambda do zapewnić program obsługi zdarzeń.

Notatka

W przypadku zgodności z poprzednimi wersjami, jeśli tylko jeden parametr wejściowy ma nazwę _, wówczas w wyrażeniu lambda _ jest traktowana jako nazwa tego parametru.

Począwszy od języka C# 12, można podać wartości domyślne dla parametrów w wyrażeniach lambda. Składnia i ograniczenia dotyczące domyślnych wartości parametrów są takie same jak w przypadku metod i funkcji lokalnych. Poniższy przykład deklaruje wyrażenie lambda z parametrem domyślnym, a następnie wywołuje je raz przy użyciu wartości domyślnej i raz z dwoma jawnymi parametrami:

var IncrementBy = (int source, int increment = 1) => source + increment;

Console.WriteLine(IncrementBy(5)); // 6
Console.WriteLine(IncrementBy(5, 2)); // 7

Wyrażenia lambda można również zadeklarować za pomocą params tablic lub kolekcji jako parametrów:

var sum = (params IEnumerable<int> values) =>
{
    int sum = 0;
    foreach (var value in values) 
        sum += value;
    
    return sum;
};

var empty = sum();
Console.WriteLine(empty); // 0

var sequence = new[] { 1, 2, 3, 4, 5 };
var total = sum(sequence);
Console.WriteLine(total); // 15

W ramach tych aktualizacji, gdy grupa metod z domyślnym parametrem jest przypisana do wyrażenia lambda, wyrażenie lambda ma również ten sam parametr domyślny. Grupę metod z parametrem kolekcji params można również przypisać do wyrażenia lambda.

Wyrażenia lambda z domyślnymi parametrami lub kolekcjami params jako parametry nie mają typów naturalnych odpowiadających typom Func<> lub Action<>. Można jednak zdefiniować typy delegatów, które zawierają domyślne wartości parametrów:

delegate int IncrementByDelegate(int source, int increment = 1);
delegate int SumDelegate(params int[] values);
delegate int SumCollectionDelegate(params IEnumerable<int> values);

Można też użyć niejawnie typowanych zmiennych z deklaracjami var do zdefiniowania typu delegata. Kompilator syntetyzuje prawidłowy typ delegata.

Aby uzyskać więcej informacji na temat parametrów domyślnych w wyrażeniach lambda, zobacz specyfikację funkcji dla parametrów domyślnych w wyrażeniach lambda.

Asynchroniczne lambdy

Można łatwo tworzyć wyrażenia lambda i instrukcje, które zawierają przetwarzanie asynchroniczne przy użyciu słów kluczowych async i await. Na przykład poniższy przykład Windows Forms zawiera obsługę zdarzeń, która wywołuje i oczekuje na asynchroniczną metodę, ExampleMethodAsync.

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);
    }
}

Tę samą procedurę obsługi zdarzeń można dodać przy użyciu asynchronicznego wyrażenia lambda. Aby dodać tę procedurę obsługi, dodaj modyfikator async przed listą parametrów lambda, jak pokazano w poniższym przykładzie:

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);
    }
}

Aby uzyskać więcej informacji na temat tworzenia i używania metod async, zobacz Programowanie asynchroniczne z async i await.

Wyrażenia i krotki lambda

Język C# zapewnia wbudowaną obsługę krotek . Możesz podać krotkę jako argument wyrażenia lambda, a wyrażenie lambda może również zwrócić krotkę. W niektórych przypadkach kompilator języka C# używa wnioskowania typu w celu określenia typów składników krotki.

Krotka jest definiowana przez umieszczenie listy jej elementów oddzielonych przecinkami w nawiasach. W poniższym przykładzie użyto krotki z trzema składnikami, aby przekazać sekwencję liczb do wyrażenia lambda, które podwaja każdą wartość i zwraca krotkę z trzema składnikami zawierającymi wynik mnożenia.

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)

Zazwyczaj pola krotki mają nazwę Item1, Item2itd. Można jednak zdefiniować krotkę z elementami nazwanymi, jak pokazuje poniższy przykład.

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}");

Aby uzyskać więcej informacji na temat krotek C#, zobacz typy krotek.

Lambdy ze standardowymi operatorami zapytań

LINQ to Objects, podobnie jak inne implementacje, ma parametr wejściowy, którego typ jest jednym z Func<TResult> rodziny delegatów ogólnych. Ci delegaci używają parametrów typu do definiowania liczby i typu parametrów wejściowych oraz zwracanego typu delegata. Func delegaty są użyteczne do enkapsulacji wyrażeń zdefiniowanych przez użytkownika, które są stosowane do każdego elementu w zestawie danych źródła. Rozważmy na przykład typ delegata Func<T,TResult>:

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

Delegat może być utworzony jako instancja Func<int, bool>, gdzie int jest parametrem wejściowym, a bool jest wartością zwracaną. Wartość zwracana jest zawsze określona w ostatnim parametrze typu. Na przykład Func<int, string, bool> definiuje delegata z dwoma parametrami wejściowymi, int i stringoraz zwracanym typem bool. Po wywołaniu następującego delegata Func zwraca wartość logiczną wskazującą, czy parametr wejściowy jest równy pięciu:

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

Można również podać wyrażenie lambda, gdy typ argumentu jest Expression<TDelegate>, na przykład w standardowych operatorach zapytań zdefiniowanych w typie Queryable. Po określeniu argumentu Expression<TDelegate> element lambda jest kompilowany do drzewa wyrażeń.

W poniższym przykładzie użyto standardowego operatora zapytania Count:

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)}");

Kompilator może wywnioskować typ parametru wejściowego lub można go również jawnie określić. To konkretne wyrażenie lambda zlicza te liczby całkowite (n), które po podzieleniu przez dwa mają pozostałą część 1.

Poniższy przykład tworzy sekwencję zawierającą wszystkie elementy w tablicy numbers poprzedzających 9, ponieważ jest to pierwsza liczba w sekwencji, która nie spełnia warunku:

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

Poniższy przykład określa wiele parametrów wejściowych poprzez umieszczenie ich w nawiasach. Metoda zwraca wszystkie elementy w tablicy numbers, dopóki nie znajdzie liczby, której wartość jest mniejsza niż położenie porządkowe w tablicy:

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

Wyrażenia lambda nie są używane bezpośrednio w wyrażeniach zapytań , ale można ich używać w wywołaniach metod w wyrażeniach zapytania, jak pokazano w poniższym przykładzie:

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

Wnioskowanie typu w wyrażeniach lambda

Podczas pisania lambda często nie trzeba określać typu parametrów wejściowych, ponieważ kompilator może wywnioskować typ na podstawie treści lambda, typów parametrów i innych czynników, jak opisano w specyfikacji języka C#. W przypadku większości standardowych operatorów zapytań pierwsze dane wejściowe to typ elementów w sekwencji źródłowej. Jeśli wykonujesz zapytanie dotyczące IEnumerable<Customer>, zmienna wejściowa jest wnioskowana jako obiekt Customer, co oznacza, że masz dostęp do jego metod i właściwości:

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

Ogólne reguły wnioskowania typów dla wyrażeń lambda są następujące:

  • Lambda musi zawierać taką samą liczbę parametrów jak typ delegata.
  • Każdy parametr wejściowy w lambda musi być niejawnie konwertowany na odpowiadający mu parametr delegata.
  • Wartość zwracana przez lambda (jeśli istnieje) musi być można niejawnie przekonwertować na typ zwracany delegata.

Naturalny typ wyrażenia lambda

Wyrażenie lambda w sobie nie ma typu, ponieważ wspólny system typów nie ma wewnętrznej koncepcji "wyrażenia lambda". Jednak czasami wygodnie jest mówić nieformalnie o "typie" wyrażenia lambda. Ten "typ nieformalny" odnosi się do typu delegata lub typu Expression, na który jest konwertowane wyrażenie lambda.

Wyrażenie lambda może mieć typ naturalny. Zamiast wymuszać deklarowanie typu delegata, takiego jak Func<...> lub Action<...> dla wyrażenia lambda, kompilator może wywnioskować typ delegata z wyrażenia lambda. Rozważmy na przykład następującą deklarację:

var parse = (string s) => int.Parse(s);

Kompilator może wnioskować, że parse jest Func<string, int>. Kompilator wybiera dostępnego delegata Func lub Action, jeśli istnieje odpowiedni. W przeciwnym razie syntetyzuje typ delegata. Na przykład typ delegata jest syntetyzowany, jeśli wyrażenie lambda ma parametry ref. Gdy wyrażenie lambda ma typ naturalny, można go przypisać do mniej jawnego typu, takiego jak System.Object lub System.Delegate:

object parse = (string s) => int.Parse(s);   // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>

Grupy metod (czyli nazwy metod bez list parametrów) z dokładnie jednym przeciążeniem mają typ naturalny:

var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose

Jeśli przypiszesz wyrażenie lambda do System.Linq.Expressions.LambdaExpression, lub System.Linq.Expressions.Expression, a lambda ma naturalny typ delegata, wyrażenie ma naturalny typ System.Linq.Expressions.Expression<TDelegate>, z naturalnym typem delegata używanym jako argument parametru typu:

LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s);       // Expression<Func<string, int>>

Nie wszystkie wyrażenia lambda mają typ naturalny. Rozważ następującą deklarację:

var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda

Kompilator nie może wywnioskować typu parametru dla s. Gdy kompilator nie może wywnioskować typu naturalnego, należy zadeklarować typ:

Func<string, int> parse = s => int.Parse(s);

Określony typ zwracany

Zazwyczaj zwracany typ wyrażenia lambda jest oczywisty i wywnioskowany. W przypadku niektórych wyrażeń, które nie działają:

var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type

Można określić typ zwracany wyrażenia lambda przed parametrami wejściowymi. Po określeniu jawnego typu zwracanego należy w nawiasie określić parametry wejściowe:

var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>

Atrybuty

Atrybuty można dodać do wyrażenia lambda i jego parametrów. W poniższym przykładzie pokazano, jak dodać atrybuty do wyrażenia lambda:

Func<string?, int?> parse = [ProvidesNullCheck] (s) => (s is not null) ? int.Parse(s) : null;

Możesz również dodać atrybuty do parametrów wejściowych lub wartości zwracanej, jak pokazano w poniższym przykładzie:

var concat = ([DisallowNull] string a, [DisallowNull] string b) => a + b;
var inc = [return: NotNullIfNotNull(nameof(s))] (int? s) => s.HasValue ? s++ : null;

Jak pokazano w poprzednich przykładach, podczas dodawania atrybutów do wyrażenia lambda lub jego parametrów, należy umieścić w nawiasy parametry wejściowe.

Ważny

Wyrażenia lambda są wywoływane za pomocą bazowego typu delegata. Różni się to od metod i funkcji lokalnych. Metoda Invoke delegata nie sprawdza atrybutów w wyrażeniu lambda. Atrybuty nie mają żadnego wpływu, gdy jest wywoływane wyrażenie lambda. Atrybuty w wyrażeniach lambda są przydatne do analizy kodu i można je odnaleźć za pomocą odbicia. Jedną z konsekwencji tej decyzji jest to, że System.Diagnostics.ConditionalAttribute nie można zastosować do wyrażenia lambda.

Przechwytywanie zmiennych zewnętrznych i zakresu zmiennych w wyrażeniach lambda

Lambdy mogą odwoływać się do zmiennych zewnętrznych. Te zmienne zewnętrzne to zmienne, które znajdują się w zakresie metody definiującej wyrażenie lambda lub w zakresie typu, który zawiera to wyrażenie lambda. Zmienne przechwycone w ten sposób są przechowywane do późniejszego użycia w wyrażeniu lambda, nawet jeśli zmienne w przeciwnym razie wyjdą z zakresu i zostaną usunięte z pamięci. Zmienna zewnętrzna musi być bezwarunkowo przypisana, zanim będzie mogła być użyta w wyrażeniu lambda. W poniższym przykładzie przedstawiono następujące reguły:

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
}

Następujące reguły dotyczą zakresu zmiennych w wyrażeniach lambda:

  • Przechwycona zmienna nie zostanie zebrana do momentu, gdy delegat, który się do niej odwołuje, kwalifikuje się do zbierania pamięci.
  • Zmienne wprowadzone w wyrażeniu lambda nie są widoczne w otaczającej metodzie.
  • Wyrażenie lambda nie może bezpośrednio przechwytywać parametru w, reflub out z metody zawierającej.
  • Instrukcja zwraca w wyrażeniu lambda nie powoduje zwrócenia otaczającej metody.
  • Wyrażenie lambda nie może zawierać instrukcji goto, breaklub continue, jeśli cel tej instrukcji skoku znajduje się poza blokiem wyrażenia lambda. Jest to również błąd, gdy polecenie skoku znajduje się poza blokiem wyrażenia lambda, jeśli element docelowy jest wewnątrz tego bloku.

Modyfikator static można zastosować do wyrażenia lambda, aby zapobiec przypadkowemu przechwyceniu zmiennych lokalnych lub stanu wystąpienia przez lambdę:

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

Statyczna funkcja lambda nie może przechwytywać zmiennych lokalnych ani stanu instancji z otaczających zakresów, ale może odwoływać się do członków statycznych i definicji stałych.

Specyfikacja języka C#

Aby uzyskać więcej informacji, zobacz sekcję Anonimowe wyrażenia funkcji specyfikacji języka C# .

Aby uzyskać więcej informacji na temat tych funkcji, zobacz następujące uwagi dotyczące propozycji funkcji:

Zobacz też