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]
Після ще += Handler3 [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;

Лямбди та підписка всередині циклу
Якщо ви, наприклад, у циклі підписуєтеся лямбдами, переконайтеся, що розумієте, що відбувається зі змінними, які лямбда «захоплює». Легко випадково захопити не те значення, яке ви очікували.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ