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. Практичне застосування на співбесідах і в проєктах
У багатьох проєктах із моделлю publish/subscribe грамотна організація подій — запорука масштабованості та підтримуваності. На співбесідах нерідко просять:
- реалізувати систему подій із правильною типізацією,
- показати керування життєвим циклом підписників,
- пояснити потокобезпечний виклик подій.
Чистий, задокументований і коректно організований подієвий код одразу вирізняє вас серед кандидатів.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ