JavaRush /Курси /C# SELF /Стандартний шаблон подій ( ...

Стандартний шаблон подій ( EventHandler/ EventArgs)

C# SELF
Рівень 52 , Лекція 3
Відкрита

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
public event Action<int> MyEvent;
Ні Так Низька Ні
public delegate void MyHandler(object, int); event MyHandler ...
Так Так Середня Ні (рідко)
public event EventHandler;
Так Так (EventArgs) Висока Так, стандарт
public event EventHandler<MyArgs>;
Так Так (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....

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ