1. Вступ
Отже, коли ми говоримо про «подію» (event) у контексті C#, маємо на увазі механізм, що дає змогу безпечно сповістити один або кілька об’єктів, що щось сталося. Події — не магія: «під капотом» це делегати (ключове слово delegate) з додатковим захистом від неправильного використання.
Уявіть підписку на YouTube-канал. На каналі (об’єкті — власнику події) є кнопка «Підписатися»: будь-який глядач (інший об’єкт) може натиснути її й потрапити до списку підписників (делегатів-обробників). Коли автор викладає нове відео (викликає подію), сповіщення отримують лише підписники, і тільки автор каналу вирішує, коли це відбудеться. Важлива деталь: глядачі можуть підписатися або відписатися, але не можуть надіслати сповіщення самостійно — це право має лише власник каналу.
Трохи формального синтаксису
// Оголошення делегата, що визначає форму обробника події
public delegate void SimpleEventHandler();
// Оголошення події на основі делегата
public event SimpleEventHandler SomethingHappened;
Усе просто: event — це ключове слово для оголошення події, а тип події завжди визначається делегатом.
2. Перший простий приклад: подія «Кнопку натиснули!»
Крок 1. Визначаємо делегат для обробників події
// Обробник події: нічого не повертає і не приймає параметрів
public delegate void ButtonClickHandler();
Крок 2. Клас із подією
public class Button
{
// Подія: можна підписуватися й відписуватися, але викликати її ззовні не можна
public event ButtonClickHandler Click;
// Метод, що імітує натискання кнопки
public void Press()
{
Console.WriteLine("Кнопку натиснуто!");
// Виклик події: повідомити всіх підписників
Click?.Invoke();
}
}
Зверніть увагу: подію оголошено на основі делегата, а в методі Press подію викликаємо через ?.Invoke(). Чому так? Тому що подія може бути порожньою (ніхто не підписався), тоді Click дорівнює null. Оператор безпечного виклику гарантує, що обробники викликаються лише за наявності підписників.
Крок 3. Підписка на подію та запуск коду
// Приклад використання
public class Program
{
public static void Main()
{
var button = new Button();
// Додаємо слухача (підписуємося на подію)
button.Click += OnButtonClicked;
button.Click += () => Console.WriteLine("Ще один обробник!");
button.Press(); // Симулюємо натискання кнопки
// Можна відписатися від події
button.Click -= OnButtonClicked;
button.Press();
}
// Звичайний обробник події
public static void OnButtonClicked()
{
Console.WriteLine("Обробник: Кнопку було натиснуто!");
}
}
Під час першого натискання відпрацюють обидва обробники, під час другого — лише лямбда.
3. Чим подія відрізняється від делегата
Делегату можна вільно присвоювати значення і навіть повністю замінювати список підписників — хтось може написати щось на кшталт button.Click = null, і всі попередні підписки буде скасовано.
З подією все інакше — вона жорсткіше прив’язана до об’єкта. Лише власник класу, у якому подію оголошено, може викликати її безпосередньо (наприклад, Click() зсередини класу). Будь-який інший код може тільки підписуватися через += або відписуватися через -=, але не може скинути всіх підписників разом. Такий підхід захищає інкапсуляцію та не дає порушити систему підписок.
4. Сигнатура: типи делегатів для подій, параметри
У .NET прийнято, щоб обробник подій отримував два параметри — object sender (хто викликав подію) і аргументи події (EventArgs). Рекомендується використовувати делегати EventHandler або EventHandler<TEventArgs>.
Приклад: делегат із параметрами
public delegate void ButtonClickHandler(object sender, EventArgs e);
EventArgs — базовий клас для передавання додаткової інформації. Якщо потрібно більше даних, створюємо свій похідний клас.
Застосуймо це до нашої кнопки
// Клас аргументів події
public class ButtonClickEventArgs : EventArgs
{
public string UserName { get; }
public ButtonClickEventArgs(string userName)
{
UserName = userName;
}
}
public class Button
{
public event EventHandler<ButtonClickEventArgs> Click;
public void Press(string userName)
{
Console.WriteLine("Кнопку натиснуто!");
Click?.Invoke(this, new ButtonClickEventArgs(userName));
}
}
public static void Main()
{
var button = new Button();
button.Click += OnButtonClicked;
button.Press("Василь");
}
public static void OnButtonClicked(object sender, ButtonClickEventArgs e)
{
Console.WriteLine($"Користувач {e.UserName} натиснув кнопку!");
}
5. Візуальна схема: як працює подія
+-------------+
| Користувач |
+-------------+
|
v
+--------------+
| Button.Press|
+--------------+
|
v
+-----------------+ +------------------------+
| Виклик Click? |----->---| Підписник 1 |
+-----------------+ +------------------------+
| | Виконати обробник |
v +------------------------+
+-----------------+ +------------------------+
| Якщо є підписник|----->-| Підписник 2 |
+-----------------+ +------------------------+
: | Виконати обробник |
v +------------------------+
— Button.Press викликає подію; обробники підписників виконуються по черзі.
6. Корисні нюанси
Рекомендована форма подій у .NET
Використовуйте EventHandler і EventHandler<TEventArgs>, щоб код був сумісний із бібліотеками та інструментами. Такий підхід спрощує еволюцію подій: ви додаєте нові властивості до своїх EventArgs, не порушуючи роботу підписників.
Документація: EventHandler, event.
Події vs делегати
Якщо треба зв’язати один компонент з одним конкретним методом — достатньо делегата (delegate). Якщо ж потрібно, щоб різні частини програми могли підписуватися та відписуватися будь-коли — використовуйте подію (event).
7. Типові помилки й незручні моменти під час створення подій
Помилка № 1: виклик події без перевірки на null. Якщо у події немає підписників, пряма спроба викликати її призведе до NullReferenceException. Користуйтеся безпечним викликом:
Click?.Invoke(...);
Помилка № 2: спроба викликати подію поза класом. Подію можна ініціювати (викликати) лише всередині того класу, де її оголошено. З іншого класу компілятор видасть помилку — це захищає інкапсуляцію.
Помилка № 3: багаторазова підписка на один і той самий обробник. Якщо один обробник підписано кілька разів, він буде викликаний стільки ж разів. Це особливість механізму підписки. Стежте за дублюванням += і коректною відпискою -=.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ