1. Переваги лямбда-виразів
У програмуванні часто повторюються схожі завдання. Наприклад: «відфільтруй список користувачів старших за 18 років», «порахуй суму всіх чисел, що задовольняють умові», «відсортуй товари за ціною». Без лямбд такі завдання доводилося вирішувати створенням окремих методів — це зайвий шум у коді, особливо якщо потрібна дія використовується лише в одному місці. Лямбди роблять код компактнішим і ближчим до того, як ми подумки формулюємо завдання.
Лямбда-вирази — це стандартний інструмент у багатьох сучасних мовах (не лише в C#), бо вони дозволяють «передавати поведінку» як значення, будь то фільтр, обробник або функція перетворення.
1. Стислість і лаконічність
Лямбда-вирази дають змогу позбутися довгих оголошень зайвих методів або анонімних делегатів, коли ви пишете невеликий шматок функціональності «на льоту». Наприклад, ось як виглядав би фільтр списку чисел до появи лямбд:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
// До появи лямбд:
List<int> evenNumbers = numbers.FindAll(delegate(int x) { return x % 2 == 0; });
// З лямбда-виразом:
List<int> evenNumbers2 = numbers.FindAll(x => x % 2 == 0);
Результат той самий, але код із лямбдою значно компактніший. У великих проєктах економія рядків коду стає відчутною.
2. Підвищення читабельності та виразності
Лямбди дозволяють зосередитися на суті операції, прибираючи «шум» синтаксису. Ваш код стає ближчим до природної мови:
var adults = users.Where(user => user.Age >= 18);
Порівняйте це з оголошенням окремого методу bool IsAdult(User user), який довелося б писати лише заради цього фільтра.
3. Зручна інтеграція з LINQ та API колекцій
Головна сила лямбд — у поєднанні з LINQ та колекціями. Багато методів стандартних колекцій і LINQ-оператори очікують функцію як параметр (наприклад, Func<T, bool> для фільтрації). Лямбда дає змогу безпосередньо на місці оголосити потрібну функцію:
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. Захоплення змінних із зовнішньої області видимості (closures)
Лямбда-вираз може використовувати змінні, оголошені ззовні. Це дає гнучкість і дозволяє створювати динамічні функції «на льоту»:
int minAge = 18;
var filtered = users.Where(u => u.Age >= minAge); // minAge "захоплено" лямбдою
Це відкриває цікаві шаблони для генерації функцій із «налаштованими параметрами».
Цікавий факт: Усередині лямбди можна не лише читати, а інколи й змінювати зовнішні змінні, хоча робити це варто обережно — більше про це в лекції про замикання!
5. Вбудований і контекстний код
Лямбда-вирази «живуть» там само, де й використовуються, а не шукаються по всьому проєкту серед методів. Це робить код ближчим до принципу «максимум інформації в мінімумі простору».
У прикладах розвитку нашого застосунку (нагадаємо, ми розробляємо мінісистему обліку книжок у бібліотеці) припустімо, що маємо такий список книжок:
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
public int Year { get; set; }
public double Price { get; set; }
}
// Десь у коді:
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 },
// ...
};
// Знайти всі книжки, дорожчі за 300:
var expensiveBooks = books.Where(b => b.Price > 300).ToList();
Ви бачите умови відбору прямо поруч із викликом, не витрачаючи час на пошуки сторонніх функцій у коді.
6. Використання як callback-ів, подій, таймерів
Лямбди чудово підходять для задання одноразової дії, наприклад обробника події (callback-а):
button.Click += (sender, args) => Console.WriteLine("Кнопку натиснуто!");
Лямбда-вирази усувають потребу писати окремі методи, якщо обробник дуже простий.
7. Розширення можливостей делегатів
Раніше для передачі поведінки доводилося оголошувати іменовані методи; тепер ви буквально пишете функцію на місці:
Timer timer = new Timer(_ => Console.WriteLine("Тік!"), null, 0, 1000);
8. Спрощення тестування та впровадження залежностей
За допомогою лямбд можна легко створювати підробні (mock) реалізації поведінки для тестів, не засмічуючи основний код утилітними проєктами або тимчасовими класами. Наприклад, якщо конструктор приймає делегат, то для тесту ви передаєте лямбду з потрібною поведінкою.
2. Головні недоліки лямбда-виразів
Як і будь-який потужний інструмент, лямбда-вирази не безгрішні. Розгляньмо, які складнощі та обмеження вони можуть створювати.
1. Втрата читабельності за надмірної вкладеності
Лямбди хороші, доки їх не надто багато в одному місці. Вкладені лямбди або довгі лямбди роблять код складним для розуміння:
var result = items.Select(x => x.Children.Where(y => y.Value > 10)
.Select(z => z.Name.ToUpper())
.ToList());
Додайте ще кілька рівнів — і отримаєте «головоломку для читання коду».
Порада: якщо лямбда перевищує 3–4 рядки — винесіть її в окремий іменований метод. Не бійтеся здатися ретроградом: читабельність важливіша за модні скорочення.
2. Складнощі з налагодженням
Лямбди не дуже дружні до налагоджувача, особливо якщо вони написані «в один рядок» прямо в ланцюжку викликів LINQ. Іноді складно поставити точку зупину всередині лямбди або подивитися значення змінних на конкретному етапі.
Щоб спростити налагодження, можна на час винести тіло лямбди в іменований метод або розбити довгі ланцюжки LINQ на ділянки з проміжними змінними.
3. Неочевидність типів аргументів і повернених значень
Лямбда-вираз часто передається як делегат (Func<...>, Action<...>, Predicate<T>). Іноді буває складно одразу зрозуміти, якими саме мають бути типи вхідних параметрів і тип поверненого значення, особливо в методах із generic-параметрами.
Наприклад:
Func<int, string, double> myFunc = (a, b) => a + b.Length; // Ой! Повернеться int, а має бути double.
Компілятор підкаже помилку, але новачку відразу буває непросто зрозуміти, що якась лямбда «не вписалася у форму».
4. Проблеми із захопленням змінних
Захоплення змінних зовнішньої області видимості — палиця з двома кінцями. Якщо використовувати захоплені змінні неуважно, можна отримати неочікуваний результат. Наприклад, у циклі:
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions) action();
Багато хто очікує на виході 0 1 2, а отримуємо 3 3 3. Чому? На момент виконання лямбди змінна i уже дорівнює 3! Лямбда «захопила» саму змінну, а не її значення.
Це типова помилка для новачків, докладно про неї в офіційній документації. Вирішується — але потребує обережності.
5. Втрата явних імен і проблеми з повторним використанням
Лямбда-вирази хороші для одноразових дій. Але якщо одна й та сама умова/функція використовується в кількох місцях, варто винести логіку в іменований метод. Інакше ризикуєте отримати дублювання й помилки під час правок.
6. Незручно додавати XML-коментарі
Лямбду не можна супроводити XML-документацією для автогенерації довідки (як це робиться для методів). Коментарі до лямбда-виразів доводиться писати в тілі коду звичайними коментарями.
7. Можливі проблеми з продуктивністю
У більшості випадків лямбди не відчутно повільніші за звичайні методи. Проте за частого та масового створення лямбд із захопленням змінних вони призводять до виділення додаткових обʼєктів (closure). У місцях, критичних до продуктивності (наприклад, у щільному циклі або у високонавантажених сервісах), варто замислитися — чи не дешевше використати статичні методи.
8. Не можна використовувати оператори goto, break, continue поза циклами
Якщо лямбду оголошено всередині циклу, у ній безпосередньо не можна використовувати break або continue щодо зовнішнього циклу — це синтаксично неприпустимо.
9. Лямбда-вираз не охоплює всього поведінкового різноманіття
Лямбди не вміють напряму працювати з атрибутами, не можна вказувати модифікатори доступу, не можна виконувати деякі спеціальні дії — наприклад, оголошувати локальні функції з іменем.
3. Вибір із двох лих
Коли лямбда-вирази корисні
| Сценарій | Лямбда — зручно? | Чому |
|---|---|---|
| Коротка фільтрація/перетворення | 👍 | Швидко й зрозуміло |
| Багаторівневі вкладені операції | 👎 | Код стане нечитабельним |
| Re-use (повторне використання) | 👎 | Краще винести в метод |
| Callback-логіка, події | 👍 | Компактно |
| Опис складної бізнес-логіки | 👎 | Потрібні назва й коментарі |
| Робота з LINQ | 👍 | Ідеальний сценарій |
Коли все ж краще відмовитися від лямбд
- Якщо логіка довга й містить багато розгалужень і обчислень.
- Якщо лямбда робить щось неочевидне для читача і не має пояснення.
- Якщо функцію треба задокументувати, використовувати в кількох місцях або дати їй «промовисту» назву.
- Якщо лямбду використовують надто глибоко у вкладених викликах — є ризик втратити читабельність.
4. Типові помилки під час роботи з лямбда-виразами
Помилка із захопленням змінних у циклі:
List<Action> actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var act in actions) act(); // Усі виведуть 5!
Як правильно:
for (int i = 0; i < 5; i++)
{
int captured = i; // захоплюємо окрему змінну
actions.Add(() => Console.WriteLine(captured));
}
Надто довга лямбда:
books.Where(b => b.Price > 1000 && b.Title.Contains("C#") && b.Author.Length > 4 && бла-бла-бла...);
// Код став нечитабельним, винесіть у метод!
Використання лямбди там, де потрібен метод із документацією:
Якщо функцію використовують багато разів або її треба детально прокоментувати, краще написати іменований метод:
bool IsExpensiveBook(Book book) => book.Price > 1000;
books.Where(IsExpensiveBook);
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ