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;
}

Далее используем универсальный делегат с дженериком — 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) Высочайшая Да, стандарт

Практическая польза на собеседованиях и в боевом коде

Использование стандартного шаблона событий — must-have для .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....

2
Задача
C# SELF, 52 уровень, 3 лекция
Недоступна
Создание события с EventHandler<TEventArgs> и кастомным EventArgs
Создание события с EventHandler<TEventArgs> и кастомным EventArgs
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ