1. Синтаксис подписки на событие
Представьте приложение для управления задачами: когда задача завершена, срабатывает событие, и обработчик может, например, отправить уведомление, обновить UI или записать информацию в лог. C# делает подписку на события удобной и безопасной: вы явно обозначаете, что конкретный обработчик реагирует на конкретное событие.
Подписка на событие в C# — это почти как добавить себя в список гостей на вечеринку:
publisher.MyEvent += HandlerMethod;
Здесь publisher ― объект, который объявил событие MyEvent, а HandlerMethod ― метод, который будет вызван при возникновении этого события.
Давайте посмотрим на это в контексте минимального примера. Допустим, у нас есть приложение для учёта количества нажатий:
public class Clicker
{
public event Action Clicked;
public void Click()
{
// Что-то было кликнуто!
Clicked?.Invoke();
}
}
Вот наш "издатель" события Clicked. Теперь — подпишем обработчик:
Clicker clicker = new Clicker();
void OnClicked()
{
Console.WriteLine("Кнопка была нажата!");
}
clicker.Clicked += OnClicked;
// Где-то в коде
clicker.Click();
// → "Кнопка была нажата!"
Как это работает? На событие "нажатие" мы добавили наш метод OnClicked, и он будет вызываться каждый раз, когда происходит клик.
2. Обработчики событий: какие бывают и как их объявлять
Обработчик события — это метод, который будет вызван, когда сработает событие. Его сигнатура должна совпадать с типом делегата события. Например, если у нас событие объявлено как public event Action Clicked;, то обработчик должен быть методом без параметров и возвращаемого значения.
Обработчик для Action
void OnClicked()
{
Console.WriteLine("Событие произошло (Action)!");
}
Обработчик для стандартного EventHandler
Когда вы используете классический подход с EventHandler, обработчик принимает два параметра: отправителя (object sender) и данные события (EventArgs e):
public class Alarm
{
public event EventHandler AlarmRaised;
public void RaiseAlarm()
{
AlarmRaised?.Invoke(this, EventArgs.Empty);
}
}
Alarm alarm = new Alarm();
void AlarmHandler(object sender, EventArgs e)
{
Console.WriteLine("Сработала тревога!");
}
alarm.AlarmRaised += AlarmHandler;
alarm.RaiseAlarm();
Анонимные методы и лямбда-выражения
C# позволяет использовать анонимные функции и лямбда-выражения напрямую при подписке:
clicker.Clicked += () => Console.WriteLine("Another click!");
Или чуть сложнее, если событие с аргументами:
alarm.AlarmRaised += (sender, e) =>
{
Console.WriteLine($"Alarm raised by: {sender}");
};
4. Полезные нюансы
Подписка и отписка: важные нюансы
Настоящая жизнь — это когда гостей на вечеринке становится слишком много, или кто-то хочет уйти домой. С делегатами всё то же самое: обработчик можно добавить (подписаться) и убрать (отписаться):
// Подписка
publisher.MyEvent += MyHandler;
// Отписка (когда обработчик больше не нужен)
publisher.MyEvent -= MyHandler;
Почему это важно? Если не отписываться, особенно в больших приложениях, обработчики могут остаться "висящими" и не дать сборщику мусора очистить объекты — отсюда утечки памяти.
Несколько обработчиков
На одно событие можно подписать сколько угодно обработчиков. Они вызовутся по очереди в том порядке, в котором были добавлены.
clicker.Clicked += () => Console.WriteLine("Первый обработчик!");
clicker.Clicked += () => Console.WriteLine("Второй обработчик!");
clicker.Click();
// → Первый обработчик!
// → Второй обработчик!
Вы даже можете подписывать и отписывать обработчики прямо "на лету" — это гибко и удобно.
Почему это действительно важно: реальные сценарии
- UI (Windows Forms, WPF, WinUI, MAUI): клик по кнопке, изменение текста в поле — всё это события.
- FileSystemWatcher: уведомление о появлении новых файлов в папке.
- Асинхронные операции: завершение загрузки файла, прогресс выполнения задач.
- Система плагинов: отдельные модули подписываются на события основного приложения.
Событийная модель позволяет строить расширяемые архитектуры: вы можете добавлять новые модули и подписываться на уже существующие события без изменения базового кодa.
Примеры подписки
| Сигнатура события | Пример подписки | Пример обработчика |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
5. Типичные ошибки и подводные камни
Ошибка №1: несовпадение сигнатур обработчика.
Если событие объявлено как event Action<int>, а вы пытаетесь подписать метод без параметров, компилятор выдаст ошибку. Всегда проверяйте, что метод соответствует требуемой сигнатуре события.
Ошибка №2: захват переменных в лямбдах.
При подписке через лямбда-выражение оно может захватывать переменные из окружающей области видимости (см. тему «Замыкания»). Если после подписки переменная изменится, обработчик будет видеть уже новое значение, что может привести к неожиданным результатам.
Ошибка №3: подписка на неинициализированное событие.
Если вы подписываетесь на событие до того, как создан и инициализирован объект, содержащий это событие, вы рискуете получить NullReferenceException. Перед подпиской убедитесь, что объект готов к использованию.
Ошибка №4: множественная подписка одного обработчика.
Если один и тот же обработчик подписан на событие несколько раз, он будет вызван столько же раз. Это не баг, а особенность работы событий, но часто оказывается неприятным сюрпризом.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ