1. Вступ
У багатьох сучасних C#-проєктах майже немає «звичних» методів для обробки подій. Так сталося, бо лямбда-вирази дозволяють швидко й лаконічно оголосити обробник прямо в місці підписки (оператор +=), якщо обробка проста і не використовується повторно в інших частинах коду. Це схоже на те, ніби ви приклеїли невеличку нотатку прямо на кавомашину з інструкцією «натисніть цю кнопку» замість того, щоб писати детальний посібник і зберігати його в окремій теці. Якщо завдання локальне й разове, лямбда — ідеальна!
Де це використовується на практиці
- В ASP.NET (наприклад, під час обробки подій життєвого циклу сторінки),
- У WPF/WinForms для UI (наприклад, під час натискання кнопки),
- У серверному програмуванні (наприклад, логіка всередині конвеєра),
- У тестах, коли обробнику не потрібне окреме імʼя.
Синтаксис: як це виглядає в коді
Спочатку розгляньмо звичайну обробку події:
// Оголошення події
public event EventHandler? MyEvent;
// Підписка на подію звичайним методом
void Handler(object? sender, EventArgs e)
{
Console.WriteLine("Подія сталася!");
}
public void Subscribe()
{
MyEvent += Handler;
}
А тепер — варіант з лямбда-виразом (анонімна функція):
public void Subscribe()
{
MyEvent += (sender, e) => Console.WriteLine("Подія сталася (лямбда)!");
}
Зверніть увагу: ми не створюємо окремий метод, а задаємо обробник прямо під час підписки на подію. Сигнатура лямбди автоматично збігається з типом події (EventHandler).
Порівняння підходів
| Звичайний метод | Лямбда | |
|---|---|---|
| Обсяг коду | Більше (метод + підписка) | Менше, усе на місці |
| Повторне використання | Можна використовувати повторно | Зазвичай ні |
| Локальність коду | Розкиданий | Усе поруч |
| Зrozумілість/читабельність | Добре для складної логіки | Чудово для простих випадків |
2. Застосунок з обробкою подій і лямбдами
Базова структура
public class Menu
{
public event EventHandler? ItemSelected;
public void SelectItem(int index)
{
Console.WriteLine($"Пункт меню {index} обрано.");
ItemSelected?.Invoke(this, EventArgs.Empty);
}
}
Підписка з лямбда-виразом
class Program
{
static void Main()
{
var menu = new Menu();
// Підписка на подію через лямбду
menu.ItemSelected += (sender, e) =>
{
Console.WriteLine("Дякуємо за ваш вибір! Лямбда-обробник спрацював.");
};
menu.SelectItem(1);
}
}
Очікуване виведення:
Пункт меню 1 обрано.
Дякуємо за ваш вибір! Лямбда-обробник спрацював.
Захоплення змінних у зовнішньому контексті
Одна з особливостей лямбда-виразів — вони можуть «памʼятати» значення із зовнішньої області видимості. Наприклад, рахувати, скільки разів було обрано пункт меню: змінна counter буде захоплена замиканням.
static void Main()
{
var menu = new Menu();
int counter = 0;
menu.ItemSelected += (s, e) =>
{
counter++;
Console.WriteLine($"Пункт обрано {counter} раз(ів)!");
};
menu.SelectItem(1);
menu.SelectItem(2);
}
Очікуване виведення:
Пункт меню 1 обрано.
Пункт обрано 1 раз!
Пункт меню 2 обрано.
Пункт обрано 2 рази!
Це магія замикань у дії: змінна counter продовжує «жити» всередині лямбди!
Приклад із параметрами події
Якщо ваша подія використовує EventHandler<T>, де T — власний клас із додатковою інформацією, лямбда-вираз просто «підлаштовується» під потрібну сигнатуру.
public class MenuItemSelectedEventArgs : EventArgs
{
public int ItemIndex { get; }
public string Description { get; }
public MenuItemSelectedEventArgs(int itemIndex, string description)
{
ItemIndex = itemIndex;
Description = description;
}
}
public class Menu
{
public event EventHandler<MenuItemSelectedEventArgs>? ItemSelected;
public void SelectItem(int index, string description)
{
Console.WriteLine($"Пункт меню {index}: {description} обрано.");
ItemSelected?.Invoke(this, new MenuItemSelectedEventArgs(index, description));
}
}
// Використання
static void Main()
{
var menu = new Menu();
// Лямбда з розпаковкою аргументів події
menu.ItemSelected += (sender, args) =>
{
Console.WriteLine($"Обрано пункт #{args.ItemIndex}: {args.Description.ToUpper()}");
};
menu.SelectItem(3, "Про програму");
}
Очікуване виведення:
Пункт меню 3: Про програму обрано.
Обрано пункт #3: ПРО ПРОГРАМУ
3. Локальні обробники та лямбда
Лямбди ідеальні, коли:
- Логіка обробки коротка й зрозуміла,
- Обробник використовується лише в одному місці,
- Потрібно «замкнути» змінні з локального контексту.
Якщо обробка складна, потребує повторного використання або може бути викликана поза місцем оголошення, краще використати окремий іменований метод.
Приклад: з логікою всередині і зовні
Лямбда (ідеально):
button.Click += (s, e) => MessageBox.Show("Кнопку натиснуто!");
Методи (коли логіка складніша або потрібна повторно):
button.Click += Button_Click;
void Button_Click(object sender, EventArgs e)
{
if (UserConfirmed())
{
SaveData();
MessageBox.Show("Дані збережено!");
}
}
4. Під капотом: що відбувається з лямбда-обробниками
Лямбда — це теж делегат, як і звичайний обробник. Компілятор «на льоту» створює анонімний метод, а якщо в ньому є захоплені змінні, то й прихований клас для їхнього зберігання.
Важливо памʼятати (і це часта помилка): якщо ви створюєте лямбду всередині циклу і підписуєте її на подію, усі ітерації можуть захопити одну й ту саму змінну циклу.
for (int i = 0; i < 5; i++)
{
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"Клік на кнопці #{i}");
};
}
Для всіх обробників номер кнопки може виявитися рівним 5! Щоб цього уникнути, створіть копію змінної всередині циклу:
for (int i = 0; i < 5; i++)
{
int buttonIndex = i; // Локальна копія
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"Клік на кнопці #{buttonIndex}");
};
}
Тепер усе працює, як очікувалося.
Вся міць лямбда-обробників: реальне життя
Такі лямбда-вирази економлять час і прискорюють розробку, особливо коли логіка проста. Вони роблять код «ближчим до задачі», не розкидаючи його по різних файлах і класах. У реальних проєктах ви зустрінете їх повсюди: від обробки подій в UI до підписки на повідомлення в асинхронних шинах подій.
5. Типові помилки
Помилка № 1: забути відписатися (-=) у довгоживучих обʼєктів.
Якщо обʼєкт-підписник не відписався від події видавця, то посилання на делегат утримує підписника в памʼяті — збирач сміття не зможе його зібрати, навіть якщо на обʼєкт більше немає інших посилань. У результаті виникають «витоки» памʼяті й висячі залежності, особливо коли видавець живе довго (статичні події, синглтони, сервіси).
Як уникати: завжди відписуйтеся під час вивільнення/знищення (наприклад, Dispose, OnDisable, OnDestroy). Розгляньте слабкі посилання (weak events / WeakEventManager), використання патерна «менеджер подій» або IObservable/Rx, якщо сценарій складний.
Помилка № 2: захоплення змінних із циклу без локальної копії.
Типова пастка — писати підписку всередині циклу, і замикання хапає одну й ту саму змінну циклу, тож усі обробники бачать її фінальне значення, а не те, що було під час створення обробника. Це призводить до неочікуваних результатів (усі обробники «друкують» одне й те саме число тощо).
Як уникати: всередині циклу створюйте локальну копію значення й захоплюйте її:
for (int i = 0; i < n; i++)
{
int current = i;
button.Click += (s, e) => Handle(current);
}
Або передавайте потрібне значення в метод-обгортку. Це надійніше й робить намір очевидним.
Помилка № 3: використання громіздких лямбд у місці підписки.
Якщо ви кладете велику бізнес-логіку прямо в анонімну функцію під час підписки, код стає важкочитним, його складно тестувати й відлагоджувати. Плюс — з анонімними лямбдами важче коректно відписатися, бо потрібен той самий екземпляр делегата. У результаті логіка розповзається кодом і втрачає структуру.
Як уникати: виносьте складну логіку в іменовані методи або сервіси; за потреби зберігайте делегат у змінній/полі, щоб мати можливість відписатися; залишайте в лямбді лише невелику обгортку/переспрямування. Це покращить читабельність і керованість коду.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ