1. Практические сценарии использования событий
События — это не просто красивая теория из учебников. На практике они нужны почти в каждом втором приложении, а если вы работаете с UI или сетевыми сервисами — то в большинстве случаев.
Давайте рассмотрим несколько типичных сценариев из реальной разработки. Заодно посмотрим, как стандарты и лучшие практики помогают избежать распространённых ошибок.
Реагируем на пользовательские действия (как в UI)
Возможно, самый частый сценарий — когда пользователь что-то делает (нажимает кнопку, выбирает элемент в списке), и приложение должно отреагировать. Именно так работают Windows Forms, WPF, UWP, Avalonia, MAUI и другие UI-фреймворки.
Мини-пример (без UI — имитируем кнопку):
public class Button
{
public event EventHandler? Click; // стандартный делегат
public void SimulateClick()
{
Click?.Invoke(this, EventArgs.Empty); // "Кнопка" была "нажата"
}
}
class Program
{
static void Main()
{
var button = new Button();
button.Click += (sender, e) => Console.WriteLine("Кнопка нажата!");
button.SimulateClick(); // > Кнопка нажата!
}
}
В реальном UI-фреймворке событие Click срабатывает, когда пользователь жмёт на кнопку мышкой или тапает по экрану. Всё, что подписалось на это событие, получит уведомление — так и работает вся логика приложений.
Сигнализация прогресса, завершения операций и ошибок
Представьте: загрузка файла, обработка данных, длительный расчёт. Надо информировать о прогрессе либо об ошибках. Часто организуют событие на «шаг прогресса» и отдельное событие на «завершено» или «ошибка».
Пример загрузчика файлов:
public class FileDownloader
{
public event EventHandler<int>? ProgressChanged;
public event EventHandler? DownloadCompleted;
public event EventHandler<string>? DownloadFailed;
public void Download()
{
for (int i = 1; i <= 100; i += 10)
{
Thread.Sleep(50); // имитация задержки
ProgressChanged?.Invoke(this, i);
}
DownloadCompleted?.Invoke(this, EventArgs.Empty);
}
}
var downloader = new FileDownloader();
downloader.ProgressChanged += (s, progress) => Console.WriteLine($"Загрузка: {progress}%");
downloader.DownloadCompleted += (s, e) => Console.WriteLine("Загрузка завершена.");
downloader.Download();
Здесь сразу несколько событий: можно подписаться сразу на всё (или только на нужные). Это похоже на то, как работают многие стандартные .NET‑классы для потоков, загрузки файлов, работы с HTTP и пр.
Реакция на жизненный цикл объектов (например, «сохранено», «удалено»)
Допустим, у вас есть доменная модель. Вы хотите, чтобы, когда пользователь сохранил или удалил объект, какие-то процессы отреагировали: обновили кэш, лог, отправили сообщение и прочее.
public class UserRepository
{
public event EventHandler<UserEventArgs>? UserSaved;
public event EventHandler<UserEventArgs>? UserDeleted;
public void Save(User user)
{
//... сохраняем пользователя
UserSaved?.Invoke(this, new UserEventArgs(user));
}
public void Delete(User user)
{
//... удаляем пользователя
UserDeleted?.Invoke(this, new UserEventArgs(user));
}
}
public class UserEventArgs : EventArgs
{
public User User { get; }
public UserEventArgs(User user) => User = user;
}
Теперь разные модули могут подписаться и — без прямых связей! — получать уведомления обо всех изменениях пользователей. Причём сам репозиторий не знает, кто «подключился».
Асинхронные события и многопоточность
Иногда события генерируются не из основного (UI) потока — например, из фоновых задач, таймеров или асинхронных операций. В таких случаях важно помнить, что код обработчиков будет выполняться в «чужом» потоке. Если обработчик пытается обновить элементы интерфейса напрямую из другого потока — это приведёт к ошибкам.
Что такое маршалинг? Маршалинг — это передача выполнения кода из одного потока в другой, обычно из фонового потока обратно в UI‑поток, чтобы безопасно обновлять интерфейс. В UI‑приложениях (WinForms, WPF) для этого используются механизмы вроде SynchronizationContext или Dispatcher, которые позволяют «перенаправить» вызов обработчика на нужный поток.
Не делайте так:
// Событие вызывается из фонового потока, а обработчик обновляет UI напрямую — получите исключение!
Рекомендуется: проверять поток выполнения и при необходимости делать маршалинг на UI‑поток (через SynchronizationContext или Dispatcher). В консольных и серверных приложениях таких ограничений обычно нет.
Event Aggregator / Messaging
В больших приложениях часто используют централизованный агрегатор событий (Event Aggregator). Он снижает связанность: подписчики и издатели не знают друг о друге и обмениваются сообщениями через центр.
public class EventAggregator
{
public event EventHandler<SomeEventArgs>? SomeEvent;
public void Publish(SomeEventArgs args)
{
SomeEvent?.Invoke(this, args);
}
}
2. Лучшие шаблоны и практики
Используйте атрибут [CallerMemberName] для ошибок
Если создаёте инфраструктурный код (например, логгеры, трейсеры), логгируйте имя метода‑источника события с помощью атрибута CallerMemberName — это упрощает диагностику.
Старайтесь делать события thread-safe, если требуется
События мультикастовые, а обращения из разных потоков требуют осторожности: перед вызовом скопируйте делегат в локальную переменную.
var handler = MyEvent;
if (handler != null) handler(this, args);
В C# 6+ используйте безопасный вызов: MyEvent?.Invoke(this, args).
Оформляйте пользовательские EventArgs как immutable
Определяйте свои типы EventArgs с только для чтения свойствами — это предотвращает случайное изменение состояния события подписчиками.
public class WorkCompletedEventArgs : EventArgs
{
public string Message { get; }
public WorkCompletedEventArgs(string message) => Message = message;
}
public или private event
Делайте события public event, а вызов инкапсулируйте в protected virtual метод OnEventName. Это обеспечивает расширяемость (наследник может переопределить поведение), а внешние потребители не смогут вызвать событие напрямую.
Не нарушайте последовательность жизненного цикла
Если вы инициируете длинную цепочку обработчиков, помните: кто‑то может подписаться и попытаться изменить внутреннюю логику. Документируйте, где и когда вызываются события, и может ли событие сработать несколько раз.
Множественные события и композиция
Когда один объект слушает несколько источников, группируйте подписку и отписку в одном месте (например, методы Subscribe/Unsubscribe). При необходимости храните приватные поля‑делегаты для удобной отписки.
3. Как НЕ стоит использовать события: типичные ошибки
Ошибка №1: игнорирование стандартного паттерна событий.
Если каждый класс объявляет свои собственные делегаты для каждого события, это быстро превращается в хаос. Старайтесь использовать стандартные делегаты EventHandler или EventHandler<T>, где T наследуется от EventArgs. Такой подход делает код понятнее и привычнее для .NET‑разработчиков.
Ошибка №2: некорректный вызов события из внешнего кода.
Никогда не вызывайте событие напрямую вне класса‑издателя. Событие инициируется только внутри класса, обычно через защищённый метод OnEventName. Подписчики имеют право только подписываться (+=) и отписываться (-=).
Плохо:
foo.MyEvent(); // ошибка! Нельзя вызывать событие извне напрямую
Хорошо:
// Только внутри foo:
// protected virtual void OnMyEvent()
// {
// MyEvent?.Invoke(this, EventArgs.Empty);
// }
Ошибка №3: вызов события без проверки на наличие подписчиков (null).
Если попытаться вызвать событие, у которого нет подписчиков, получите NullReferenceException. В C# 6.0+ используйте ?.Invoke(...) — событие будет вызвано только при наличии подписчиков.
Ошибка №4: забыть отписаться от события (утечка памяти).
Если объект подписался на событие, но не отписался перед своим уничтожением, издатель удерживает ссылку на подписчика. Сборщик мусора не сможет освободить память, что критично при долгоживущих издателях и кратковременных подписчиках (например, ViewModel, формы). Лучшее решение — явная отписка или использование слабых ссылок (WeakReference) там, где это уместно.
Ошибка №5: вызывать событие вне защищённого виртуального метода OnEvent.
Вызывая событие напрямую, вы лишаете наследников возможности корректно переопределять поведение. Правильный паттерн — объявлять protected virtual метод, который вызывает событие.
Стандартный пример:
protected virtual void OnSomething(EventArgs e)
{
Something?.Invoke(this, e);
}
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ