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 |
|---|---|---|---|---|
|
Нет | Да | Низкая | Нет |
|
Да | Да | Средняя | Нет (редко) |
|
Да | Нет (EventArgs) | Высокая | Да, стандарт |
|
Да | Да (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....
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ