JavaRush /Курсы /C# SELF /Детальный разбор подписки ( +...

Детальный разбор подписки ( +=)

C# SELF
53 уровень , 0 лекция
Открыта

1. Введение

События в C# — это не просто переменные, в которые можно записать делегат. Это защищённый список обработчиков, и только владелец события может запустить его выполнение; остальные могут лишь добавлять (+=) или убирать (-=) свои реакции.

Например, вот так можно подписаться на событие:

worker.WorkCompleted += Worker_WorkCompleted;

На первый взгляд, это похоже на обычное сложение, но на деле работает иначе. Под капотом событие хранит цепочку вызова (invocation list) — набор делегатов, которые нужно вызвать при срабатывании события. Когда вы пишете +=, в этот список добавляется новый обработчик.

Давайте разберёмся подробнее, что происходит внутри, как формируется эта цепочка и какие нюансы могут возникнуть при подписке.

Что такое делегат-цепочка?

Напомним, делегаты в C# — "мультикастовые": им можно присвоить несколько методов, и они все выполнятся по очереди, если делегат вызовут. События используют этот механизм: их значение — это, по сути, делегат со списком обработчиков.

В терминах кода:

public event EventHandler<WorkCompletedEventArgs> WorkCompleted;

Когда кто-то подписывается:

worker.WorkCompleted += MyHandler;

C# под капотом делает примерно так:

  • Берёт текущий делегат (список обработчиков).
  • Вызывает для него метод Delegate.Combine (объединяет обработчики).
  • Записывает обновлённую цепочку обратно в переменную события.

Схематично:

Операция Внутренний список обработчиков события
До null или [Handler1]
После += Handler2 [Handler1, Handler2]
После ещё += H3 [Handler1, Handler2, Handler3]

Интересный факт: механизм мультикаст-делегатов — это не "магия", а вполне конкретная реализация: делегат под капотом содержит массив методов, которые нужно вызвать.

2. Как работает подписка: объяснение на пальцах

Давайте рассмотрим всё на конкретном примере. Пусть у нас есть издатель (Worker) и подписчики (Logger, Notifier):

public class Worker
{
    public event EventHandler<WorkCompletedEventArgs> WorkCompleted;

    public void DoWork()
    {
        // ... работа ...
        OnWorkCompleted("Задание завершено!");
    }

    protected virtual void OnWorkCompleted(string message)
    {
        WorkCompleted?.Invoke(this, new WorkCompletedEventArgs { Message = message });
    }
}

public class Logger
{
    public void LogWorkCompleted(object? sender, WorkCompletedEventArgs e)
    {
        Console.WriteLine("Лог: " + e.Message);
    }
}

public class Notifier
{
    public void ShowNotification(object? sender, WorkCompletedEventArgs e)
    {
        Console.WriteLine("Уведомление: " + e.Message);
    }
}

В Main:

var worker = new Worker();
var logger = new Logger();
var notifier = new Notifier();

worker.WorkCompleted += logger.LogWorkCompleted;
worker.WorkCompleted += notifier.ShowNotification;

// Запуск работы
worker.DoWork();

Когда вызывается OnWorkCompleted, событие сначала вызывает logger.LogWorkCompleted, потом notifier.ShowNotification (в том порядке, в каком вы подписались).

3. Полезные нюансы

Визуализация: как событие хранит обработчики

+---------------------+
| Worker              |
|---------------------|
| WorkCompleted Event |    ---> [ LogWorkCompleted, ShowNotification ]
+---------------------+

При подписке каждый новый обработчик "цепляется" к существующему списку. После вызова события делегат по очереди вызывает все подписанные методы.

Множественная подписка одним методом

worker.WorkCompleted += logger.LogWorkCompleted;
worker.WorkCompleted += logger.LogWorkCompleted; // Дважды!

В этом случае обработчик будет вызван столько раз, сколько он подписан — соответственно, здесь два раза подряд.

Подписка лямбда-выражением

worker.WorkCompleted += (sender, e) => Console.WriteLine("Анонимный обработчик: " + e.Message);

Если такую лямбду подписать несколько раз — аналогично, она будет вызвана несколько раз при каждом событии. Однако не теряйте бдительности: у каждой лямбды свой объект-делегат, нельзя так просто отписаться (подробнее см. в лекции 260 и далее).

Как работает подписка внутри: разбор на низком уровне

Событие — это особое свойство с двумя методами доступа (add/remove), которые вызываются при использовании += и -=. Если упростить, то компилятор генерирует примерно такой код:

// Примерно так (упрощённо)
public event EventHandler<WorkCompletedEventArgs> WorkCompleted
{
    add { /* код добавления обработчика */ }
    remove { /* код удаления обработчика */ }
}

По умолчанию используется стандартная реализация: делегат комбинируется с помощью Delegate.Combine и удаляется через Delegate.Remove.

Это защищает событие: извне нельзя вызвать событие напрямую (никаких worker.WorkCompleted(...);), можно только подписаться или отписаться.

Механика подписки: рисуем схему

             +----------------------+
             |                      |
             v                      v
    +--------------------+   +----------------------+
    | LogWorkCompleted   |   | ShowNotification    |
    +--------------------+   +----------------------+
             ^                      ^
             \______________________/
                       ^
                       |
               WorkCompleted Event

Так называемый "Invocation List" — цепочка вызовов.

Зачем это важно: практическое значение

Понимание механики подписки — ключ к управлению связями между объектами. Вы можете строить сложные системы, где компоненты динамически подписываются и отписываются от событий, не создавая жёстких зависимостей. Это стандарт в UI-фреймворках, игровых движках, серверных приложениях и даже в современных архитектурах микросервисов (только там это уже на уровне очередей, но идея та же).

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

4. Что можно (и нельзя) делать извне класса

Можно

  • Подписаться на событие (+=)
  • Отписаться (-=)

Нельзя

  • Вызвать событие напрямую
  • Присвоить событию делегат напрямую (worker.WorkCompleted = ... — ошибка!)

Эти ограничения реализуются через ключевое слово event. Если бы ты объявил делегат как обычное поле:

public EventHandler<WorkCompletedEventArgs> WorkCompleted; // не event!

— любой мог бы делать всё, включая обнуление обработчиков, что создало бы бардак и потенциальные баги. Именно поэтому почти всегда использовать только event!

Можно ли подписаться одним методом на несколько событий?

Да! Это называется "мультисабскрайбинг". Например:

worker.WorkCompleted += logger.LogWorkCompleted;
anotherWorker.WorkCompleted += logger.LogWorkCompleted;

Если вы подписали один и тот же метод на события разных объектов, обработчик сработает и на то, и на другое событие. Внутри обработчика можно понять, кто вызвал событие, по параметру sender.

Реальный кейс: динамические подписки

Представим, что у нас есть приложение, в котором пользователь может запускать несколько задач параллельно. Для каждой новой задачи создаётся объект Worker, на событие которого подписывается один и тот же обработчик — например, метод логгера logger.LogWorkCompleted.

var allWorkers = new List<Worker>();
for (int i = 0; i < 10; i++)
{
    var w = new Worker();
    w.WorkCompleted += logger.LogWorkCompleted;
    allWorkers.Add(w);
}

В результате, когда любая из задач завершится, логгер получит уведомление и запишет, что и когда произошло.

Практические моменты: как увидеть подписчиков события

В обычном коде узнать, сколько обработчиков подписано на событие, невозможно напрямую (событие инкапсулирует делегат). Однако внутри класса-издателя ты можешь работать с делегатом события напрямую, например, посмотреть его GetInvocationList():

// Только внутри класса-издателя
var handlers = WorkCompleted?.GetInvocationList();
if (handlers != null)
    Console.WriteLine($"Подписчиков: {handlers.Length}");

Это пригодится, если нужно сделать нестандартную логику рассылки или отладки (хотя лучше это делать только для учебных целей!).

5. Типичные ошибки и особенности

Обработчик не добавлен? Не будет вызван!
Если вы не подписались, обработчик не вызовется никогда. Перед вызовом события всегда стоит проверка на null (иначе будет NullReferenceException).

Подписка несколько раз
Если вы подписались неоднократно на одно событие одним и тем же методом — обработчик сработает столько же раз. Иногда это ловушка: случайный дублирующий вызов += превращает одно уведомление в несколько одинаковых.

Статические/нестатические методы
Вы можете подписывать как статические, так и экземплярные методы. Главное — правильная сигнатура.

public static void StaticHandler(object? sender, WorkCompletedEventArgs e) { /* ... */ }
worker.WorkCompleted += StaticHandler;

Лямбды и подписка внутри цикла
Если вы, например, в цикле подписываетесь лямбдами, убедитесь, что понимаете, что происходит с переменными, которые лямбда "захватывает". Можно случайно захватить не то значение, что ожидали.

2
Задача
C# SELF, 53 уровень, 0 лекция
Недоступна
Динамическая подписка и просмотр подписчиков
Динамическая подписка и просмотр подписчиков
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ