Что такое событие (event) в C#?

«Что такое событие (event) в C#?» — вопрос из категории C# Core, который задают на 32% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Событие (event) в C# — это языковая конструкция, реализующая шаблон проектирования «Наблюдатель» (Observer) или «Издатель-Подписчик». Она позволяет объекту (издателю) уведомлять другие объекты (подписчиков) о возникновении какого-либо действия, не имея прямых ссылок на них.

Базовые компоненты:

  1. Делегат (тип события): Определяет сигнатуру метода, который могут вызывать подписчики (обычно EventHandler или EventHandler<TEventArgs>).
  2. Ключевое слово event: Объявляет событие как член класса. Оно инкапсулирует список вызовов делегатов (список подписчиков).
  3. Метод-инициатор: Вызывает событие, когда происходит соответствующее действие.
  4. Обработчики событий: Методы в классах-подписчиках, которые выполняются при срабатывании события.

Практический пример (UI-кнопка):

// 1. Издатель (Publisher)
public class Button
{
    // 2. Объявление события. Используется стандартный делегат EventHandler.
    public event EventHandler Clicked;

    // 3. Метод, инициирующий событие (protected virtual — стандартный шаблон).
    protected virtual void OnClicked()
    {
        // Сохраняем ссылку на делегат в локальную переменную для потокобезопасности.
        EventHandler handler = Clicked;
        // Если есть подписчики (handler не null), вызываем событие.
        handler?.Invoke(this, EventArgs.Empty);
    }

    public void SimulateClick()
    {
        Console.WriteLine("Button was pressed.");
        OnClicked(); // Запускаем уведомление подписчиков.
    }
}

// 4. Подписчик (Subscriber)
public class Logger
{
    public void LogButtonClick(object sender, EventArgs e)
    {
        Console.WriteLine($"Button clicked at {DateTime.Now}. Sender: {sender}");
    }
}

// Использование:
var myButton = new Button();
var logger = new Logger();

// Подписка на событие (добавление обработчика в список вызовов).
myButton.Clicked += logger.LogButtonClick;
// Можно подписать несколько методов.
myButton.Clicked += (s, e) => Console.WriteLine("Anonymous handler fired!");

// Имитация клика — вызовутся ВСЕ подписанные обработчки.
myButton.SimulateClick();
// Вывод:
// Button was pressed.
// Button clicked at ... Sender: Button
// Anonymous handler fired!

// Отписка от события.
myButton.Clicked -= logger.LogButtonClick;

Ключевые особенности и best practices:

  • Инкапсуляция: Вне класса-издателя событие можно только += (подписаться) и -= (отписаться), но нельзя = (перезаписать всех подписчиков) или вызвать Invoke().
  • Потокобезопасность: Перед вызовом всегда копируйте событие в локальную переменную (как в OnClicked()), чтобы избежать исключения NullReferenceException, если подписчик отпишется между проверкой на null и вызовом.
  • Отписка: Чтобы избежать утечек памяти, объекты должны отписываться от событий, время жизни которых превышает время жизни самого объекта.