1. Вступ
Згадаймо: подія — це публічний контракт, обіцянка вашого коду сповістити підписників, коли станеться щось важливе. У прикладах із попередніх лекцій можна було побачити таке оголошення:
public event Action<string> MessageSent;
Або навіть так:
public delegate void MyHandler(int value);
public event MyHandler SomethingHappened;
Це працює, але такий підхід породжує чимало проблем — від різнобою в сигнатурах методів до неможливості зʼясувати, від кого надійшла подія і що саме сталося. Уявіть, що на клавіатурі під час кожного натискання кнопка спрацьовує випадково, щоразу по-різному — і все це в одній програмі з однією й тією самою задачею. Наприклад, ви граєте у гру й натискаєте пробіл: щойно він означав стрибок, а тепер раптово відкриває інвентар. Жах, а не стандарт!
У .NET є своєрідна «конституція» для подій — домовленість щодо сигнатури обробника та структури даних, які передаються разом із подією. Ось як виглядає канонічна сигнатура:
void Handler(object sender, EventArgs args);
Знайомий рядок? Його можна побачити всюди: від клацань на кнопці у WinForms до системних подій в ASP.NET і навіть у сторонніх бібліотеках.
2. Що таке EventHandler і EventArgs
Головна ідея: кожна подія передає дві речі:
- Хто викликав подію? sender
- Що сталося? EventArgs — додаткові дані
У світі C# це оформлюється так:
public delegate void EventHandler(object sender, EventArgs e);
- object sender — посилання на ініціатора події. Це може бути що завгодно — зазвичай просто this.
- EventArgs e — обʼєкт із додатковою інформацією про подію. Для простих сценаріїв використовують EventArgs.Empty, для складніших — власні нащадки EventArgs.
Цікавий факт
В офіційних рекомендаціях .NET для публічних API прийнято, щоб подія мала сигнатуру (object sender, EventArgs e). Якщо ви бачите подію без sender і EventArgs — це, швидше за все, авторське спрощення, а не канонічний .NET-стиль.
Переваги єдиного стандарту
Єдиний стандарт спрощує логування, тестування, універсальну підписку та розширення системи. Відкрийте вихідні коди WinForms, WPF, ASP.NET — побачите один і той самий шаблон.
3. Як використовувати стандартний шаблон подій
1. Використовуємо вбудований делегат EventHandler
Замість оголошення власного делегата можна взяти готовий:
public event EventHandler SmthHappened;
Тепер обробник завжди має однакову форму:
private void OnSmthHappened(object sender, EventArgs e)
{
// Логіка реакції на подію
}
Підписка, як і раніше, проста:
myObj.SmthHappened += OnSmthHappened;
Важливо: якщо подія не передає додаткових даних, використовуйте EventArgs.Empty.
2. Створення власних аргументів подій
Якщо потрібно передати інформацію (результат обчислення, імʼя файлу, помилку), створіть нащадка від EventArgs:
public class CalculationEventArgs : EventArgs
{
public double Result { get; }
public CalculationEventArgs(double result) => Result = result;
}
Далі використовуйте універсальний generic‑делегат — EventHandler<TEventArgs>:
public event EventHandler<CalculationEventArgs> CalculationFinished;
І обробник тепер приймає саме ваш тип аргументів:
private void OnCalculationFinished(object sender, CalculationEventArgs e)
{
Console.WriteLine($"Обчислення завершено. Результат: {e.Result}");
}
4. Приклад застосунку
Продовжімо наш навчальний проєкт: створімо калькулятор, який додає або віднімає числа й повідомляє про завершення операції через подію.
Мінімалістичний приклад
public class Calculator
{
public event EventHandler<CalculationEventArgs> CalculationPerformed;
public void Add(int a, int b)
{
int result = a + b;
// Ініціюємо подію
CalculationPerformed?.Invoke(this, new CalculationEventArgs(result));
}
}
public class CalculationEventArgs : EventArgs
{
public int Result { get; }
public CalculationEventArgs(int result) => Result = result;
}
Підписуємося та використовуємо:
var calc = new Calculator();
calc.CalculationPerformed += (sender, e) =>
{
Console.WriteLine($"Результат операції: {e.Result}");
};
calc.Add(10, 20);
// Виведе: Результат операції: 30
Ось і весь «стандарт»: обробник завжди приймає обʼєкт-відправник і обʼєкт аргументів — універсально й зрозуміло.
5. Корисні нюанси
Інкапсуляція виклику події: добра практика
У .NET прийнято виносити логіку виклику події в окремий захищений метод із префіксом On:
protected virtual void OnCalculationPerformed(CalculationEventArgs e)
{
CalculationPerformed?.Invoke(this, e);
}
А всередині відповідних методів («Add», «Subtract» тощо) просто викликайте цей метод:
public void Add(int a, int b) => OnCalculationPerformed(new CalculationEventArgs(a + b));
Такий стиль дозволяє нащадкам перевизначати поведінку події та зменшує шанс забути її викликати.
Схема: як влаштована подія за стандартом .NET
graph LR
A[Обʼєкт-видавець] -- "event EventHandler/ EventHandler<TEventArgs>" --> B[Список підписників]
B -- "Метод-обробник (object sender, EventArgs e)" --> C[Реакція на подію]
A -- "this (відправник)" --> C
A -- "EventArgs (дані)" --> C
Порівняння варіантів оголошення подій
| Підхід | Передача ініціатора (sender) | Передача аргументів | Універсальність | Використання в .NET |
|---|---|---|---|---|
|
Ні | Так | Низька | Ні |
|
Так | Так | Середня | Ні (рідко) |
|
Так | Так (EventArgs) | Висока | Так, стандарт |
|
Так | Так (MyArgs) | Найвища | Так, стандарт |
Практична користь на співбесідах і в бойовому коді
Використання стандартного шаблону подій — обовʼязкова навичка для .NET‑розробника. На співбесіді вас майже напевно запитають про EventHandler і пару sender/EventArgs. Подія типу Action<T> часто вважається «спрощенням».
У реальних проєктах такий підхід спрощує спільну роботу, тестування, розширюваність і підтримку коду. Сторонні бібліотеки (логування, профайлери) легше інтегруються, якщо використано стандартну форму.
Блок-схема виклику події
flowchart TD
subgraph A[Клас-видавець]
C1((Обʼєкт))
C2[Метод, що викликає подію]
C3["event EventHandler<MyArgs>"]
end
subgraph B[Клас-підписник]
D1((Підписка))
D2["Обробник події (object sender, MyArgs args)"]
end
C1 -- Викликає --> C2
C2 -- "Invoke(this, args)" --> C3
C3 -- "Сповіщає" --> D1
D1 -- "Викликає" --> D2
6. Поради, нюанси й типові помилки
1. Сигнатура обробника. Хочете оголосити подію типу Action<int>? Це спокусливо, але ви втрачаєте sender і сумісність з екосистемою.
2. Передача аргументів. Не плутайте EventArgs зі звичайними параметрами. Усі дані для обробника передавайте в обʼєкті аргументів.
3. Використання null для аргументів. Замість null використовуйте EventArgs.Empty, якщо додаткових даних нема.
4. Слабка типізація. Не створюйте одну «всеїдну» подію EventHandler і не складайте туди все підряд. Створіть окремий клас‑нащадок для кожної події — читабельність і надійність вищі.
5. Помилки у виклику події. Завжди перевіряйте наявність підписників: SomeEvent?.Invoke(this, e). Без підписників посилання на подію дорівнює null.
6. Порушення інкапсуляції. Не викликайте подію ззовні класу‑видавця. Подія — лише для підписки/відписки; виклик робіть всередині через метод On....
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ