JavaRush /Курси /C# SELF /Обробка подій: лямбда-вирази

Обробка подій: лямбда-вирази

C# SELF
Рівень 52 , Лекція 4
Відкрита

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: використання громіздких лямбд у місці підписки.
Якщо ви кладете велику бізнес-логіку прямо в анонімну функцію під час підписки, код стає важкочитним, його складно тестувати й відлагоджувати. Плюс — з анонімними лямбдами важче коректно відписатися, бо потрібен той самий екземпляр делегата. У результаті логіка розповзається кодом і втрачає структуру.
Як уникати: виносьте складну логіку в іменовані методи або сервіси; за потреби зберігайте делегат у змінній/полі, щоб мати можливість відписатися; залишайте в лямбді лише невелику обгортку/переспрямування. Це покращить читабельність і керованість коду.

1
Опитування
Події в C#, рівень 52, лекція 4
Недоступний
Події в C#
Створення подій за допомогою делегатів
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ