JavaRush /Курсы /C# SELF /Обработка событий: лямбда-выражения

Обработка событий: лямбда-выражения

C# SELF
52 уровень , 4 лекция
Открыта

1. Введение

Во множестве современных C#-проектов почти нет "обычных" методов для обработки событий. Так сложилось потому, что лямбда-выражения позволяют быстро и лаконично объявить обработчик прямо в месте подписки (оператор +=), если обработка простая и не переиспользуется в других частях кода. Это похоже на то, как если бы вы приклеили маленькую записку прямо на кофемашину с инструкцией «нажми эту кнопку» вместо того, чтобы писать подробный мануал и хранить его в отдельной папке. Если задача локальна и одноразова, лямбда идеальна!

Где это используется на практике

  • В ASP.NET (например, при обработке событий жизненного цикла страницы),
  • В WPF/WinForms для UI (например, при клике на кнопку),
  • В серверном программировании (например, логика внутри pipeline),
  • В тестах, когда обработчику не требуется отдельное имя.

Синтаксис: как это выглядит в коде

Давайте для начала рассмотрим обычную обработку события:


// Объявление события
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).

Сравнение подходов

Обычный метод Лямбда
Объем кода Больше (метод + подписка) Меньше, всё на месте
Переиспользуемость Можно переиспользовать Обычно нет
Локальность кода Разнесён Всё рядом
Ясность/читабельность Хорошо для сложной логики Отлично для простых кейсов

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: использование громоздких лямбд в месте подписки.
Если вы помещаете крупную бизнес-логику прямо в анонимную функцию при подписке, код становится трудночитаемым, его сложно тестировать и отлаживать. Плюс — с анонимными лямбдами сложнее корректно отписаться, потому что нужен тот же экземпляр делегата. В результате логика расползается по коду и теряет структуру.
Как избегать: выносите сложную логику в именованные методы или сервисы; при необходимости храните делегат в переменной/поле, чтобы иметь возможность отписаться; оставляйте в лямбде только небольшую обёртку/перенаправление. Это улучшит читаемость и управляемость кода.

2
Задача
C# SELF, 52 уровень, 4 лекция
Недоступна
Кнопка с пользовательскими данными
Кнопка с пользовательскими данными
1
Опрос
События в C#, 52 уровень, 4 лекция
Недоступен
События в C#
Создание событий с помощью делегатов
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ