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 lambda 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 typ delegata. 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. W przeciwnym razie można go przekonwertować na jeden z ActionFunc 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 delegata Func<T,TResult> . W poniższym przykładzie wyrażenie x => x * xlambda , które określa parametr o nazwie x i zwraca wartość kwadratu x , jest przypisywane 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 drzewa 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óre wymagają wystąpień typów delegatów lub drzew wyrażeń. Jednym z przykładów jest argument metody w Task.Run(Action) celu przekazania kodu, który powinien zostać wykonany w tle. Wyrażenia lambda można również używać 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 Enumerable.Select metody w System.Linq.Enumerable klasie, na przykład w linQ to Objects i LINQ to XML, parametr jest typem System.Func<T,TResult>delegata . Podczas wywoływania metody w System.Linq.Queryable klasie, na przykład w linQ to SQL, typ parametru Queryable.Select jest typem Expression<Func<TSource,TResult>>drzewa wyrażeń . W obu przypadkach można użyć tego samego wyrażenia lambda, aby określić wartość parametru. To sprawia, że dwa Select wywołania wyglądają podobnie, chociaż w rzeczywistości typ obiektów utworzonych na podstawie wyrażeń lambda jest inny.

Wyrażenia lambdas

Wyrażenie lambda z wyrażeniem po prawej stronie => operatora jest nazywane wyrażeniem lambda. Lambda wyrażenia zwraca wynik wyrażenia i ma 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).

Instrukcja lambdas

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

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

Treść lambdy instrukcji może składać się z dowolnej liczby instrukcji, jednak w praktyce jest ich zwykle nie więcej niż dwie lub trzy.

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ślanie braku parametrów wejściowych za pomocą pustych nawiasów:

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 .

Można użyć odrzuconych , aby określić co najmniej dwa parametry wejściowe wyrażenia lambda, które nie są używane w wyrażeniu:

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

Parametry odrzucania lambda mogą być przydatne w przypadku użycia wyrażenia lambda w celu zapewnienia procedury obsługi zdarzeń.

Uwaga

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

Począwszy od języka C# 12, można podać wartości domyślne 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 params kolekcji można również przypisać do wyrażenia lambda.

Wyrażenia lambda z domyślnymi parametrami lub params kolekcjami jako parametry nie mają typów naturalnych, które odpowiadają typom lub Action<> typomFunc<>. 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 wpisanych zmiennych z deklaracjami var , aby zdefiniować typ 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 domyślnych parametrów w wyrażeniach lambda.

Asynchroniczne lambdy

Wyrażenia i instrukcje lambda można łatwo tworzyć, które zawierają przetwarzanie asynchroniczne przy użyciu słów kluczowych asynchronicznych i await . Na przykład poniższy przykład formularzy systemu Windows zawiera procedurę obsługi zdarzeń, która wywołuje metodę asynchroniową i oczekuje na metodę asynchroniową . 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);
    }
}

Można dodać ten sam program obsługi zdarzeń, używając lambdy asynchronicznej. Aby dodać tę procedurę obsługi, dodaj modyfikator przed listą async 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 asynchronicznych, zobacz Asynchroniczne programowanie z asynchroniczną funkcją asynchroniczną i await.

Wyrażenia i krotki lambda

Język C# zapewnia wbudowaną obsługę krotki. 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 dołączenie rozdzielanej przecinkami listy składników 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 nazwanymi składnikami, jak w poniższym przykładzie.

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 krotki języka C#, zobacz Typy krotki.

Lambdy ze standardowymi operatorami zapytań

Między innymi implementacje LINQ to Objects mają parametr wejściowy, którego typem jest jedna z Func<TResult> rodzin 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ą przydatne w przypadku hermetyzacji wyrażeń zdefiniowanych przez użytkownika, które są stosowane do każdego elementu w zestawie danych źródłowych. Rozważmy na przykład typ delegata Func<T,TResult> :

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

Delegat można utworzyć wystąpienie jako Func<int, bool> wystąpienie, w którym int jest parametrem wejściowym i 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 int wejściowymi i string, i zwracanym typem bool. Następujący Func delegat, po wywołaniu, zwraca wartość logiczną, która wskazuje, 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 Expression<TDelegate>to , na przykład w standardowych operatorach zapytań zdefiniowanych w typie Queryable . Po określeniu argumentu Expression<TDelegate> lambda jest kompilowana do drzewa wyrażeń.

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

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, ale można go również określić w sposób jawny. 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 numbers tablicy poprzedzającej 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, ujęcie ich w nawiasy. Metoda zwraca wszystkie elementy w numbers tablicy, 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 zapytania, 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#. Dla większości standardowych operatorów zapytań pierwszy element danych wejściowych jest typem elementów w sekwencji źródłowej. Jeśli wykonujesz zapytanie dotyczące IEnumerable<Customer>obiektu , zmienna wejściowa jest wnioskowana jako Customer obiekt, co oznacza, że masz dostęp do jej 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:

  • Wyrażenie lambda musi zawierać taką samą liczbę parametrów jak typ delegata.
  • Każdy parametr wejściowy w wyrażeniu lambda musi umożliwiać niejawną konwersję na odpowiadający mu parametr delegata.
  • Wartość zwracana wyrażenia lambda (jeżeli istnieje) musi umożliwiać niejawną konwersję na zwracany typ 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 nieformalny "typ" odnosi się do typu delegata lub Expression typu, do którego jest konwertowane wyrażenie lambda.

Począwszy od języka C# 10, 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. Na przykład przeanalizujmy następującą deklarację:

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

Kompilator może wywnioskować parse , że jest to Func<string, int>. Kompilator wybiera dostępny Func lub Action delegowany, jeśli istnieje odpowiedni. W przeciwnym razie syntetyzuje typ delegata. Na przykład typ delegata jest syntetyzowany, jeśli wyrażenie lambda ma ref parametry. 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 elementu s. Gdy kompilator nie może wywnioskować typu naturalnego, należy zadeklarować typ:

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

Jawny 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

Począwszy od języka C# 10, 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

Począwszy od języka C# 10, można dodać atrybuty 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 rozbudować nawiasy wejściowe.

Ważne

Wyrażenia lambda są wywoływane za pomocą bazowego typu delegata. Różni się to od metod i funkcji lokalnych. Metoda delegata Invoke 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ć go do wyrażenia lambda.

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

Lambdas może odwoływać się do zmiennych zewnętrznych. Te zmienne zewnętrzne to zmienne, które znajdują się w zakresie w metodzie, która definiuje wyrażenie lambda lub w zakresie w typie zawierającym wyrażenie lambda. Przechwytywane w ten sposób zmienne są przechowywane do użytku w wyrażeniu lambda, nawet gdyby w innym wypadku te zmienne znalazłyby się poza zakresem i zostałyby usunięte w ramach odśmiecania pamięci. Zewnętrzna zmienna musi być zdecydowanie przypisana, aby można jej było użyć w wyrażeniu lambda. W poniższym przykładzie pokazano te 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
}

Do zakresu zmiennych w wyrażeniach lambda są stosowane następujące reguły:

  • Przechwycona zmienna nie zostanie odebrana do momentu, gdy delegat, który odwołuje się do niego, kwalifikuje się do odzyskiwania pamięci.
  • Zmienne wprowadzone w wyrażeniu lambda nie są widoczne w otaczającej metodzie.
  • Wyrażenie lambda nie może bezpośrednio przechwycić parametru in, ref lub out z otaczającej metody.
  • Instrukcja return w wyrażeniu lambda nie powoduje zwrócenia otaczającej metody.
  • Wyrażenie lambda nie może zawierać instrukcji goto, break lub continue , jeśli element docelowy tej instrukcji skoku znajduje się poza blokiem wyrażenia lambda. Jest to również błąd dotyczący instrukcji skoku poza blokiem wyrażenia lambda, jeśli element docelowy znajduje się wewnątrz bloku.

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

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

Statyczna funkcja lambda nie może przechwytywać zmiennych lokalnych ani stanu wystąpienia z ujęć zakresów, ale może odwoływać się do statycznych elementów członkowskich 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ż