Menu

Wyrażenia lambda w C#: składnia, Func i Action, domknięcia i LINQ

Jak działają wyrażenia lambda w C#: składnia => we wszystkich formach, przechowywanie lambd w Func i Action, przekazywanie ich do metod i LINQ, lambdy z blokiem instrukcji, domknięcia nad przechwyconymi zmiennymi i błąd przechwycenia w pętli for, którego nie ma foreach.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Wyrażenie lambda to mała funkcja anonimowa zapisana w miejscu użycia operatorem => (czytanym "przechodzi w"). Parametry są po lewej, wynik po prawej:

Wynik:

60.0
12
ORDER SHIPPED!
96.0, 180.0

Lambda nie ma własnego typu. Staje się delegatem (obiektem wskazującym na funkcję) takiego typu delegatu, jakiego oczekuje kontekst: tutaj Func<decimal, decimal>, Predicate<T> dla List.FindAll, EventHandler dla zdarzenia. Delegaty Func zwracają wartość, a delegaty Action nie; całą rodzinę wymienia strona o Func i Action.

Formy składni

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

Lambda wyrażeniowa ma po => jedno wyrażenie, którego wartość jest wynikiem. Lambda z blokiem instrukcji ma blok w nawiasach klamrowych, który może zawierać dowolne instrukcje i musi zwracać wartość przez return na każdej ścieżce, jeśli delegat coś zwraca (w przeciwnym razie CS1643, Not all code paths return a value in lambda expression).

Nowsze wersje dodały kolejne formy, wszystkie opcjonalne:

  • C# 9: parametry odrzucane, więc kilka nieużywanych parametrów może mieć nazwę _ ((_, _) => Save()), oraz lambdy static, którym nie wolno przechwytywać zmiennych.
  • C# 10: "typ naturalny", więc var square = (int x) => x * x; wnioskuje Func<int, int>. Wcześniej var f = x => x * 2; się nie kompiluje; nawet w C# 10 nadal się nie kompiluje bez typu parametru, z błędem CS8917 The delegate type could not be inferred.
  • C# 12: domyślne wartości parametrów, (int x, int y = 1) => x + y.

Przekazywanie lambd do metod

Każda metoda z parametrem typu delegatu przyjmuje lambdę. Tak przekazuje się zachowanie, a nie tylko dane:

Wynik:

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

Filter nie wie, co znaczy "zachowaj"; decyduje kod wywołujący przez lambdę. List.Sort działa tak samo: lambda to porównanie zwracające wartość ujemną, zero lub dodatnią, a Sort wywołuje ją tyle razy, ile potrzebuje.

Lambdy w LINQ

LINQ to miejsce, w którym większość kodu C# spotyka się z lambdami. Każdy operator zapytania przyjmuje jakąś: Where warunek, Select projekcję, OrderBy klucz, Sum wartość do zsumowania:

Wynik:

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

Lambdy nie wykonują się tam, gdzie je zapisano. LINQ je przechowuje i wywołuje, gdy zapytanie jest wyliczane przez foreach, ToList() albo Sum(); nazywa się to wykonaniem odroczonym. Same operatory omawia strona o LINQ.

Domknięcia: przechwycone zmienne

Lambda może używać zmiennych lokalnych i parametrów metody, w której ją zapisano. Przechwytuje zmienną, a nie jej bieżącą wartość, a zmienna żyje tak długo jak lambda:

Wynik:

1
2
3
1
50.0

Każde wywołanie MakeCounter tworzy nowe count, więc oba liczniki są niezależne. Lambda zniżki odczytała rate w chwili wykonania, a nie zapisania, więc zastosowała 50%. Kompilator realizuje to, przenosząc przechwycone zmienne do ukrytej klasy, którą współdzielą metoda i lambda.

Pułapka przechwycenia w pętli

Przechwycenie zmiennej pętli to miejsce, w którym domknięcia gryzą. Pętla for ma jedną zmienną na całą pętlę, więc każda lambda w niej utworzona współdzieli tę zmienną:

Wynik:

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

Wszystkie trzy lambdy z pierwszej pętli wykonały się po jej zakończeniu, gdy i wynosiło 3. Skopiowanie i do zmiennej zadeklarowanej w ciele daje każdej lambdzie jej własną. foreach sam robi to poprawnie: od C# 5 jego zmienna iteracji jest nową zmienną przy każdym przejściu. Przed C# 5 miał ten sam błąd, dlatego starsze odpowiedzi w internecie nadal zalecają kopię także dla foreach.

Ta sama pułapka dotyczy wszystkiego, co wykonuje się później: procedur obsługi zdarzeń, timerów, Task.Run i zapytań LINQ zbudowanych w pętli i wyliczanych po niej.

Lambdy, metody anonimowe i funkcje lokalne

Lambdy zastąpiły składnię metod anonimowych z C# 2, która nadal się kompiluje:

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

Dla pomocnika używanego tylko w jednej metodzie C# 7.0 oferuje też funkcje lokalne, które mają nazwę, mogą być rekurencyjne i nie alokują delegatu, dopóki go z nich nie utworzysz. Używaj lambdy, gdy przekazujesz zachowanie do czegoś (LINQ, Sort, zdarzenie); funkcji lokalnej albo metody prywatnej, gdy wywołujesz ją bezpośrednio.

Jeszcze jedna różnica pojawia się przy dostawcach LINQ, takich jak Entity Framework: lambda przypisana do Expression<Func<T, bool>> nie jest w ogóle kompilowana do kodu, tylko do struktury danych opisującej kod, którą dostawca tłumaczy na SQL. Składnia jest identyczna; o tym, co dostaniesz, decyduje typ parametru wywoływanej metody.

Najczęściej zadawane pytania

Czym jest wyrażenie lambda w C#?

To funkcja anonimowa zapisana operatorem =>: x => x * 2 przyjmuje x i zwraca x * 2. Lambda nie ma własnej nazwy; jest konwertowana na typ delegatu, taki jak Func<int, int> albo Action<string>, i można ją przechować w zmiennej, przekazać do metody albo zwrócić z metody.

Jak napisać lambdę z wieloma parametrami albo bez parametrów?

Umieść parametry w nawiasach: (a, b) => a + b dla dwóch, () => DateTime.Now dla żadnego. Tylko pojedynczy parametr może obejść się bez nawiasów (x => x + 1). Możesz też jawnie podać typy: (int a, int b) => a + b.

Czym jest domknięcie w lambdzie C#?

Lambda, która używa zmiennej lokalnej z otaczającej metody, przechwytuje tę zmienną, a nie migawkę jej wartości. Zmienna żyje tak długo jak lambda, a jeśli któraś strona później ją zmieni, druga zobaczy tę zmianę. Dzięki temu działają liczniki i callbacki i właśnie to powoduje klasyczny błąd przechwycenia w pętli for.

Dlaczego wszystkie lambdy w pętli for widzą tę samą wartość?

Pętla for ma jedną zmienną na całą pętlę, więc każda lambda utworzona w środku przechwytuje to samo i i widzi jego końcową wartość, gdy wykona się później. Skopiuj ją do zmiennej lokalnej w ciele (int copy = i;) i przechwyć kopię. foreach nie ma tego problemu od C# 5, bo tworzy nową zmienną w każdej iteracji.

Dlaczego var f = x => x * 2 się nie kompiluje?

Kompilator nie wie, jakiego typu delegatu chcesz ani jakiego typu jest x, więc zgłasza CS8917 The delegate type could not be inferred (przed C# 10 CS0815). Zadeklaruj typ (Func<int, int> f = x => x * 2;) albo, w C# 10 i nowszych, podaj typ parametru: var f = (int x) => x * 2;.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ