1. Стиль объявления и именования событий
События — не просто делегаты. Это отдельная сущность для коммуникации между частями приложения, и её объявление должно быть понятным.
Используйте правильный тип делегата
В 99% случаев используйте стандартные делегаты:
- EventHandler — для событий без данных.
- EventHandler<TEventArgs> — когда нужно передать параметры.
Стандартизация облегчает поддержку кода и интеграцию с библиотеками .NET. Не изобретайте свой делегат, если подходит EventHandler.
public event EventHandler SomethingHappened; // Нет данных
public event EventHandler<MyEventArgs> DataReceived; // Есть дополнительные данные
Если нужна особая кастомизация — объявляйте собственный делегат, но это редкость.
Именование событий
В .NET события называют в прошедшем времени: Completed, Clicked, Changed, Received. Это подчёркивает факт произошедшего.
Примеры:
public event EventHandler DataLoaded; // Данные были загружены
public event EventHandler<MessageEventArgs> MessageReceived; // Сообщение получено
public event EventHandler Saving; // Начался процесс сохранения
Иногда используют форму Changing для событий "до" изменения, чтобы дать шанс вмешаться.
2. Организация класса-издателя: виртуальный метод OnEvent
Всегда добавляйте защищённый виртуальный метод, который вызывает событие: централизованная точка вызова, расширяемость при наследовании и предсказуемость поведения.
public class FileLoader
{
public event EventHandler<FileLoadedEventArgs> FileLoaded;
protected virtual void OnFileLoaded(FileLoadedEventArgs e)
{
FileLoaded?.Invoke(this, e);
}
public void Load(string filename)
{
// ... логика загрузки файла ...
OnFileLoaded(new FileLoadedEventArgs(filename));
}
}
public class FileLoadedEventArgs : EventArgs
{
public string FileName { get; }
public FileLoadedEventArgs(string fileName) => FileName = fileName;
}
Пусть только OnFileLoaded вызывает событие — так проще сопровождать и тестировать.
3. Правила подписки и отписки: жизненный цикл, IDisposable
Если срок жизни подписчика меньше, чем у издателя, обязательно отпишитесь до уничтожения подписчика. Удобно реализовать IDisposable и отписываться в Dispose().
public class TemporaryListener : IDisposable
{
private readonly Publisher _publisher;
public TemporaryListener(Publisher publisher)
{
_publisher = publisher;
_publisher.DataReceived += HandleData;
}
private void HandleData(object sender, EventArgs e)
{
// Работа с данными
}
public void Dispose()
{
_publisher.DataReceived -= HandleData;
}
}
// Использование с using:
using (var listener = new TemporaryListener(myPublisher))
{
// listener слушает события здесь
}
// После выхода из using - Dispose вызван, отписка произошла
Если забыть отписаться, издатель удержит ссылку на делегат подписчика — получите утечку памяти и "зомби-объекты".
4. Потокобезопасный вызов событий
В многопоточном коде подписчики могут добавляться/удаляться параллельно с вызовом события. Это чревато гонками и NullReferenceException. Используйте потокобезопасный шаблон: копируйте делегат в локальную переменную.
protected virtual void OnSomethingHappened()
{
EventHandler handler = SomethingHappened;
handler?.Invoke(this, EventArgs.Empty);
}
С C# 6+ достаточно:
SomethingHappened?.Invoke(this, EventArgs.Empty);
5. Использование EventArgs вместо object
Не передавайте данные через object и поля класса. Используйте строгую типизацию через наследников EventArgs.
public class DownloadCompletedEventArgs : EventArgs
{
public string FileName { get; }
public long Size { get; }
public DownloadCompletedEventArgs(string fileName, long size)
{
FileName = fileName;
Size = size;
}
}
public event EventHandler<DownloadCompletedEventArgs> DownloadCompleted;
6. Документирование событий и подписчиков
Документируйте: когда вызывается событие, смысл полей EventArgs, нужен ли отписка и когда.
/// <summary>
/// Событие возникает после успешной загрузки данных.
/// </summary>
public event EventHandler<DataLoadedEventArgs> DataLoaded;
7. Сводные рекомендации по архитектуре событий
Разделяйте обязанности
Издатель только оповещает о факте. Подписчик сам решает, когда подписаться и отписаться.
Избегайте "бомбёжки" событиями
Не генерируйте одно и то же событие десятки раз в секунду без надобности — это избыточная нагрузка.
Старайтесь не использовать события для двусторонней связи
События — для схемы "один сообщает — много слушают". Для двусторонней коммуникации рассмотрите интерфейсы, коллбеки или другие механизмы.
Не храните в классе подписчиков
Не удерживайте явные ссылки на подписчиков — события и делегаты сделают это автоматически.
8. Классические антипаттерны
Безтиповые события
public event Action<object> SomethingHappened; // Не ясно, что там внутри
Плохо: сломана типизация, нужны касты, теряется поддерживаемость.
Забыли про отписку
public class ShortLivedListener
{
public ShortLivedListener(Publisher p) =>
p.DataReceived += DoWork;
private void DoWork(object sender, EventArgs e) { /* ... */ }
// Нет Dispose, нет отписки => зомби-объекты!
}
Нарушение SRP
Класс одновременно и издатель, и подписчик, и обработчик — смешение ролей. Разделяйте ответственность.
9. Практическое применение на собеседованиях и в проектах
Во многих проектах с публикацией-подпиской грамотная организация событий — залог масштабируемости и поддержки. На собеседованиях нередко просят:
- реализовать систему событий с правильной типизацией,
- показать управление жизненным циклом подписчиков,
- объяснить потокобезопасный вызов событий.
Чистый, документированный, корректно организованный событийный код сразу выделяет вас среди кандидатов.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ