1. Zalety wyrażeń lambda
W programowaniu często powtarzają się podobne zadania. Na przykład: "odfiltruj listę użytkowników starszych niż 18 lat", "policz sumę wszystkich liczb spełniających warunek", "posortuj produkty po cenie". Bez lambd takie zadania trzeba było rozwiązywać przez tworzenie oddzielnych metod — a to dodatkowy szum w kodzie, zwłaszcza jeśli dana operacja używana jest tylko w jednym miejscu. Lambdy sprawiają, że kod jest bardziej kompaktowy i bliższy temu, jak myślimy o zadaniu.
Wyrażenia lambda to standardowe narzędzie w wielu współczesnych językach (nie tylko w C#), bo pozwalają "przekazywać zachowanie" jako wartość, czy to filtr, handler czy funkcja transformacji.
1. Zwięzłość i lakoniczność
Wyrażenia lambda pozwalają pozbyć się długich deklaracji zbędnych metod lub anonimowych delegatów, gdy piszesz mały kawałek funkcjonalności "na miejscu". Na przykład, tak wyglądałby filtr listy liczb przed pojawieniem się lambd:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
// Przed lambdami:
List<int> evenNumbers = numbers.FindAll(delegate(int x) { return x % 2 == 0; });
// Z wyrażeniem lambda:
List<int> evenNumbers2 = numbers.FindAll(x => x % 2 == 0);
Efekt ten sam, ale kod z lambdą dużo bardziej zwarty. W dużych projektach oszczędność linii kodu robi różnicę.
2. Zwiększenie czytelności i ekspresyjności
Lambdy pozwalają skupić się na istocie operacji, eliminując "szum" składni. Twój kod staje się bliższy językowi naturalnemu:
var adults = users.Where(user => user.Age >= 18);
Porównaj to z deklaracją oddzielnej metody bool IsAdult(User user), którą trzeba by było napisać tylko dla tego filtra.
3. Wygodna integracja z LINQ i API kolekcji
Główną siłą lambd jest współpraca z LINQ i kolekcjami. Wiele metod standardowych kolekcji i operatorów LINQ oczekuje funkcji jako parametru (np. Func<T, bool> dla filtrowania). Lambda pozwala zadeklarować potrzebną funkcję bezpośrednio na miejscu:
var expensive = products.Where(p => p.Price > 1000);
var firstBook = books.FirstOrDefault(b => b.Title.StartsWith("C#"));
var doubled = numbers.Select(n => n * 2);
4. Przechwytywanie zmiennych z zewnętrznego zakresu (closures)
Wyrażenie lambda może używać zmiennych zadeklarowanych na zewnątrz. Daje to elastyczność i pozwala tworzyć dynamiczne funkcje "na gorąco":
int minAge = 18;
var filtered = users.Where(u => u.Age >= minAge); // minAge "przechwycone" przez lambdę
To otwiera ciekawe wzorce do generowania funkcji z "dopasowanymi parametrami".
Ciekawostka: Wewnątrz lambdy można nie tylko czytać, ale czasem też modyfikować zewnętrzne zmienne, choć robić to trzeba ostrożnie — więcej o tym w wykładzie o closures!
5. Wbudowany i kontekstowy kod
Wyrażenia lambda "żyją" tam, gdzie są używane, a nie szukane w całym projekcie wśród metod. To sprawia, że kod jest bliżej zasady "maksimum informacji w minimalnej przestrzeni".
W przykładach z rozwojem naszej aplikacji (przypomnijmy, tworzymy mini-system ewidencji książek w bibliotece), załóżmy, że mieliśmy taką listę książek:
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
public int Year { get; set; }
public double Price { get; set; }
}
// Gdzieś w kodzie:
List<Book> books = new List<Book>
{
new Book { Title = "C# 9.0 in a Nutshell", Author = "Skeet", Year = 2022, Price = 350 },
new Book { Title = "CLR via C#", Author = "Richter", Year = 2019, Price = 250 },
// ...
};
// Znaleźć wszystkie książki droższe niż 300:
var expensiveBooks = books.Where(b => b.Price > 300).ToList();
Widzisz kryteria wyboru bezpośrednio przy wywołaniu, nie tracąc czasu na szukanie zewnętrznych funkcji w kodzie.
6. Użycie jako callbacki, zdarzenia, timery
Lambdy świetnie nadają się do określania jednorazowych akcji, np. handlera zdarzenia (callback):
button.Click += (sender, args) => Console.WriteLine("Przycisk kliknięty!");
Wyrażenia lambda zwalniają z konieczności pisania oddzielnych metod, jeśli handler jest prosty.
7. Rozszerzenie możliwości delegatów
Dawniej, żeby przekazać zachowanie, trzeba było deklarować nazwane metody; teraz możesz dosłownie napisać funkcję na miejscu:
Timer timer = new Timer(_ => Console.WriteLine("Tik!"), null, 0, 1000);
8. Ułatwienie testowania i wstrzykiwania zależności
Dzięki lambda możesz łatwo tworzyć mockowe implementacje zachowania do testów, nie zaśmiecając głównego kodu helperami czy tymczasowymi klasami. Jeśli konstruktor przyjmuje delegat, do testu podstawiasz lambdę z odpowiednim zachowaniem.
2. Główne wady wyrażeń lambda
Jak każdy potężny instrument, wyrażenia lambda mają swoje grzechy. Omówmy, jakie trudności i ograniczenia mogą powodować.
1. Utrata czytelności przy nadmiernym zagnieżdżeniu
Lambdy są ok, dopóki nie ma ich za dużo w jednym miejscu. Zagnieżdżone lambdy lub długie lambdy sprawiają, że kod staje się męczący do analizy:
var result = items.Select(x => x.Children.Where(y => y.Value > 10)
.Select(z => z.Name.ToUpper())
.ToList());
Dodaj kilka dodatkowych poziomów — i witaj, "powszechna łamigłówka do czytania kodu".
Porada: Jeśli lambda ma więcej niż 3–4 linie — wynieś ją do osobnej nazwanej metody. Nie bój się wydać się staroświeckim: czytelność ważniejsza niż modne skróty.
2. Trudności z debugowaniem
Lambdy nie są zbyt przyjazne dla debuggera, zwłaszcza gdy są napisane "w jednej linii" bezpośrednio w łańcuchu wywołań LINQ. Czasem ciężko ustawić breakpoint wewnątrz lambdy lub sprawdzić wartości zmiennych na danym etapie.
Aby ułatwić debugowanie, można tymczasowo przenieść ciało lambdy do nazwanej metody albo podzielić długie łańcuchy LINQ na fragmenty z zmiennymi pośrednimi.
3. Nieoczywiste typy argumentów i wartości zwracanych
Wyrażenie lambda często przekazywane jest jako delegat (Func<...>, Action<...>, Predicate<T>). Czasami trudno od razu zrozumieć, jakie dokładnie powinny być typy parametrów wejściowych i typ zwracany, szczególnie w metodach generycznych.
Na przykład:
Func<int, string, double> myFunc = (a, b) => a + b.Length; // Ups! Zwróci int, a powinno double.
Kompilator wskaże błąd, ale dla początkującego nie zawsze od razu jasne jest, że lambda "nie mieści się w formie".
4. Problemy z przechwytywaniem zmiennych
Przechwytywanie zmiennych z zewnętrznego zakresu (closure) to broń obosieczna. Jeśli używać przechwyconych zmiennych nieuważnie, można dostać nieoczekiwane wyniki. Na przykład w pętli:
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions) action();
Wielu spodziewa się wyniku 0 1 2, a otrzymujemy 3 3 3. Dlaczego? W momencie wykonania lambdy zmienna i już równa jest 3! Lambda "przechwyciła" samą zmienną, nie jej wartość.
To typowy błąd dla początkujących, więcej o tym w oficjalnej dokumentacji. Da się to rozwiązać — ale wymaga ostrożności.
5. Utrata jawnych nazw i problemy z ponownym użyciem
Lambdy są świetne do jednorazowych akcji. Ale jeśli ten sam warunek/funkcja używana jest w kilku miejscach, warto wyciągnąć logikę do nazwanej metody. W przeciwnym razie ryzykujesz duplikację i błędy przy modyfikacjach.
6. Niewygoda przy dodawaniu XML-komentarzy
Lambda nie może mieć XML-dokumentacji do automatycznego generowania helpa (jak to jest z metodami). Komentarze do lambd trzeba pisać normalnymi komentarzami w kodzie.
7. Możliwe problemy z wydajnością
W większości przypadków lambdy nie są zauważalnie wolniejsze niż zwykłe metody. Jednak przy częstym i masowym tworzeniu lambd z przechwytywaniem zmiennych prowadzą do alokacji dodatkowych obiektów (closure). W krytycznych miejscach pod kątem wydajności (np. w tight loop lub w serwisach wysokiego obciążenia) warto rozważyć użycie metod statycznych.
8. Nie można używać operatorów goto, break, continue względem zewnętrznych pętli
Jeśli lambda zadeklarowana jest wewnątrz pętli, wewnątrz niej nie można bezpośrednio użyć break ani continue w odniesieniu do zewnętrznej pętli — to składniowo niedozwolone.
9. Nie wszystko można opisać wyrażeniem lambda
Lambdy nie potrafią bezpośrednio pracować z atrybutami, nie można określić modyfikatorów dostępu, ani wykonywać niektórych specjalnych rzeczy — np. deklarować lokalnych funkcji z nazwą.
3. Wybór z dwóch złych
Kiedy wyrażenia lambda są przydatne
| Scenariusz | Lambda — wygodnie? | Dlaczego |
|---|---|---|
| Krótkie filtrowanie/transformacja | 👍 | Szybko i jasno |
| Wielopoziomowe zagnieżdżone operacje | 👎 | Stanie się nieczytelne |
| Re-use (ponowne użycie) | 👎 | Warto wyciągnąć do metody |
| Callback-logika, zdarzenia | 👍 | Kompaktowo |
| Opis złożonej logiki biznesowej | 👎 | Potrzebna nazwa + komentarze |
| Praca z LINQ | 👍 | Idealny scenariusz |
Kiedy mimo wszystko lepiej zrezygnować z lambd
- Jeśli logika jest długa i zawiera dużo rozgałęzień/obliczeń.
- Jeśli lambda robi coś nieoczywistego dla czytelnika i nie ma wyjaśnienia.
- Jeśli funkcji trzeba dodać dokumentację, użyć jej w kilku miejscach lub dać jej "mówiącą" nazwę.
- Jeśli lambda używana jest zbyt głęboko w zagnieżdżonych wywołaniach — ryzyko utraty czytelności.
4. Typowe błędy przy pracy z wyrażeniami lambda
Błąd z przechwytywaniem zmiennych w pętli:
List<Action> actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var act in actions) act(); // Wszystkie wypiszą 5!
Jak poprawnie:
for (int i = 0; i < 5; i++)
{
int captured = i; // przechwytujemy osobną zmienną
actions.Add(() => Console.WriteLine(captured));
}
Zbyt długa lambda:
books.Where(b => b.Price > 1000 && b.Title.Contains("C#") && b.Author.Length > 4 && bla-bla-bla...);
// Kod stał się nieczytelny, wyciągaj do metody!
Używanie lambdy tam, gdzie potrzebna jest metoda z dokumentacją:
Jeśli funkcja używana jest wielokrotnie albo powinna być gruntownie skomentowana, lepiej napisać nazwaną metodę:
bool IsExpensiveBook(Book book) => book.Price > 1000;
books.Where(IsExpensiveBook);
GO TO FULL VERSION