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 lambdystatic, którym nie wolno przechwytywać zmiennych. - C# 10: "typ naturalny", więc
var square = (int x) => x * x;wnioskujeFunc<int, int>. Wcześniejvar f = x => x * 2;się nie kompiluje; nawet w C# 10 nadal się nie kompiluje bez typu parametru, z błędem CS8917The 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;.