В чем разница между делегатом и событием (event) в C#?

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

Ответ

Делегат — это тип, представляющий ссылку на метод. Событие — это конструкция языка, построенная на основе делегата, которая добавляет строгую инкапсуляцию для реализации шаблона «издатель-подписчик».

Делегат (Delegate)

  • Это типобезопасный указатель на функцию.
  • Экземпляр делегата можно создавать, передавать как параметр, вызывать.
  • Нет встроенных ограничений: любой код с доступом к делегату может его вызвать, перезаписать или очистить список вызовов.

Событие (Event)

  • Это обёртка над делегатом, которая ограничивает операции с ним извне класса-издателя.
  • Реализует принцип инкапсуляции: издатель контролирует момент вызова, подписчики контролируют только подписку/отписку.

Ключевые различия на практике:

public class EventPublisher
{
    // 1. Делегат как публичное поле (плохая практика для шаблона издатель-подписчик)
    public Action? PublicDelegate;

    // 2. Событие, построенное на основе делегата Action
    public event Action? OnSomethingHappened;

    public void DoWork()
    {
        // Вызов возможен и там, и там ВНУТРИ класса-издателя
        PublicDelegate?.Invoke();
        OnSomethingHappened?.Invoke();
    }
}

public class Subscriber
{
    public void Subscribe(EventPublisher publisher)
    {
        // РАБОТАЕТ: Подписка на событие (только +=, -=)
        publisher.OnSomethingHappened += HandleEvent;

        // РАБОТАЕТ: Подписка на делегат
        publisher.PublicDelegate += HandleEvent;

        // ОШИБКА КОМПИЛЯЦИИ: Нельзя вызвать событие извне класса-издателя.
        // publisher.OnSomethingHappened?.Invoke();

        // РАБОТАЕТ (и это проблема!): Можно вызвать делегат извне, нарушив инкапсуляцию.
        publisher.PublicDelegate?.Invoke();

        // ОШИБКА КОМПИЛЯЦИИ: Нельзя перезаписать всех подписчиков события.
        // publisher.OnSomethingHappened = null;

        // РАБОТАЕТ (и это большая проблема!): Можно удалить ВСЕХ подписчиков делегата.
        publisher.PublicDelegate = null;
    }

    private void HandleEvent() => Console.WriteLine("Обработка.");
}
Сводная таблица: Операция Делегат (поле) Событие (event)
Вызов извне класса-издателя Разрешен (del.Invoke()) Запрещен (только внутри класса)
Подписка (+=) Разрешена Разрешена
Отписка (-=) Разрешена Разрешена
Полная перезапись (=) Разрешена (del = null) Запрещена (только в классе-издателе)
Цель Общий механизм обратных вызовов Безопасная реализация шаблона «издатель-подписчик»

Вывод: Используйте event, когда нужно уведомлять внешние объекты о изменениях в вашем классе. Это защищает инварианты класса и предотвращает случайное или злонамеренное вмешательство в логику уведомлений.