1. Введение
Работая с программами, вы неизбежно сталкиваетесь с ситуациями, когда одна часть должна «известить» другие о том, что произошло нечто важное. Классический пример — пользователь щёлкнул мышью, и это событие требует обработки. В повседневной жизни мы уже существуем в мире событий: на кухне засвистел чайник — вы услышали сигнал и поспешили выключить плиту. Кофе пролился на клавиатуру — сердце екнуло — и вы бросились спасать ноутбук. Программирование следует тем же законам.
Событие — это механизм, позволяющий объекту-источнику (издателю) оповещать другие объекты (подписчиков) о произошедших изменениях или действиях. Это своеобразное «я возвестил — кто внимал, тот откликнулся».
В C# события — специальная конструкция на основе типа делегата. Делегат задаёт сигнатуру обратного вызова — то, что и как будет вызвано у подписчиков. Объявление события выполняется ключевым словом event, а делегат — ключевым словом delegate.
Зачем нужны события?
- Ослабленная связь: Издатель ничего не знает о подписчиках — только подаёт сигнал.
- Гибкость архитектуры: Можно динамически добавлять и убирать обработчики, не меняя код издателя.
- Масштабируемость: Добавили нового подписчика — и он тут же начал получать уведомления.
Классический шаблон «Издатель-Подписчик»
Представим, что у нас есть класс «Пожарная сигнализация» (издатель) и класс «Человек в здании» (подписчик). Когда срабатывает сигнализация, она подаёт сигнал всем одновременно — неважно, сколько людей находится в здании и где именно. Это и есть шаблон «Издатель-Подписчик» (или Observer).
Издатель не знает, сколько и каких подписчиков имеется — он просто уведомляет, а остальные сами подписываются на уведомления или игнорируют их.
Как это работает в C#?
- Издатель: определяет событие (event), предоставляет подписку/отписку.
- Подписчик: подписывается на событие и реализует обработчик (метод-обработчик будет вызван при наступлении события).
2. События на практике: первый пример
Переходя от дома к коду, опишем простейшую модель. Допустим, у нас консольное приложение, где объект-таймер каждую секунду «тикает», а разные обработчики реагируют (например, выводят «тик» в консоль или считают количество тиков).
Шаг 1. Определяем делегат и событие
public class SimpleTimer
{
// Объявим делегат для события
public delegate void TickEventHandler(object sender, EventArgs e);
// Событие, основанное на делегате
public event TickEventHandler Tick;
public void Start(int count)
{
for (int i = 0; i < count; i++)
{
System.Threading.Thread.Sleep(1000); // имитация тика!
OnTick(); // вспыхнуть событием!
}
}
protected virtual void OnTick()
{
// Вызовем событие, если есть подписчики (Tick != null)
Tick?.Invoke(this, EventArgs.Empty);
}
}
Что здесь происходит?
- Определён делегат TickEventHandler с классической сигнатурой object sender, EventArgs e.
- Событие Tick — точка подписки для обработчиков.
- Метод Start имитирует «тикание» и по таймеру вызывает OnTick.
- В OnTick событие вызывается безопасно: Tick?.Invoke(..., EventArgs.Empty).
Шаг 2. Подписка на событие
class Program
{
static void Main()
{
var timer = new SimpleTimer();
// Подписываемся на событие Tick
timer.Tick += Timer_Tick;
timer.Start(3);
// Можно отписаться, если надо
timer.Tick -= Timer_Tick;
}
static void Timer_Tick(object sender, EventArgs e)
{
Console.WriteLine("Тик!");
}
}
Мы создаём таймер, подписываемся оператором +=, затем при каждом тике вызывается обработчик. Отписка — оператор -=.
3. Полезные нюансы
Почему события лучше «жёстких» вызовов?
Если бы SimpleTimer в OnTick напрямую писал в консоль, класс оказался бы жёстко связан с конкретным действием. События же «освобождают» код: таймер не знает, что именно будут делать подписчики — запускать ракету, логировать в файл или отправлять e-mail.
Важное отличие событий и делегатов
- Делегат — «указатель» на метод, а событие — это делегат с ограничениями доступа.
- Подписчики могут только подписываться/отписываться; вызвать событие извне нельзя — это может сделать только сам издатель.
- Чтобы объявить событие, добавьте модификатор event к типу делегата — компилятор обеспечит корректную модель доступа.
Краткая схема работы события в C#
+------------------+ +------------------------------+
| | | |
| Издатель | <------> | Подписчик |
| (Publisher/Event)| | (Subscriber/Handler) |
| | | |
+------------------+ +------------------------------+
| 1) объявляет событие | 2) подписывается на него
| 3) вызывает его | 4) реализует обработчик
Когда использовать события?
- Нужно оповестить неопределённое число слушателей о произошедшем.
- Не хочется связывать логику действий внутри класса-источника.
- UI, асинхронное взаимодействие, системные уведомления — всё вокруг событий.
Краткие особенности реализации событий в C#
- Событие нельзя вызвать «извне»: только код издателя имеет право на Invoke.
- Подписка/отписка: операторы +=/-=; обработчиков может быть несколько.
- Событие — это по сути список делегатов: при наступлении события вызываются все обработчики по порядку подписки.
- Рекомендуемые делегаты: используйте EventHandler и EventHandler<TEventArgs> для совместимости с .NET-экосистемой.
4. Типичные ошибки новичков
Забывают проверить, что есть подписчики: Tick != null. Лучше использовать безопасный вызов: Tick?.Invoke(...).
Подписываются на событие, но не отписываются, когда обработчик уже не нужен. Это может удерживать объекты в памяти и приводить к утечкам.
Пытаются «вызвать» событие из внешнего класса — компилятор не позволит. Нельзя написать что-то вроде game.GameOver(), если это событие, а не метод.
Не соблюдают сигнатуру делегата. Для событий используйте стандартные EventHandler или EventHandler<TEventArgs> — так код совместим с остальными библиотеками .NET.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ