JavaRush /Курси /C# SELF /Відписка від подій ( -=

Відписка від подій ( -=) і витоки памʼяті

C# SELF
Рівень 53 , Лекція 1
Відкрита

1. Вступ

У певному сенсі підписка на подію в C# — це як підписатися на розсилку мемів від друга: оновлення приходитимуть, доки ви не скажете «досить» і не відпишетеся. У програмуванні це особливо важливо, адже забута підписка — це не просто «ще один мем», а витік памʼяті!

Уявіть, що у вас є форма в застосунку (наприклад, додаткове вікно налаштувань). Вона підписується на подію головного вікна, щоб реагувати на зміни. Користувач закриває форму, вважаючи, що її знищено, а обробник усе ще підписаний. Форма все ще жива в памʼяті, адже головний об’єкт утримує на неї посилання через подію.

Висновок: якщо об’єкт-підписник підписався на подію видавця й «забув» відписатися, збирач сміття не видалить його з памʼяті, доки живий видавець.

Пригадаємо оператор += і покажемо -=

  • += — підписка: додає обробник у список викликів події.
  • -= — відписка: видаляє обробник зі списку викликів.

Виглядає це приблизно так:


worker.WorkCompleted += handler; // підписка
worker.WorkCompleted -= handler; // відписка

Якщо обробник додано двічі, його потрібно видалити стільки ж разів, щоб він точно зник зі списку викликів.

Трохи «під капотом»

Під капотом подія у C# — це поле-делегат (або список делегатів), і оператор += фактично викликає Delegate.Combine, а -=Delegate.Remove. Об’єкт, що підписався на подію, стає частиною графа посилань. Ось чому забута підписка — це витік памʼяті.

2. Витоки памʼяті через події: як це працює

Класична ситуація


class Window
{
    public event EventHandler Updated;

    public void SimulateUpdate()
    {
        // Імітація: надсилаємо сповіщення всім підписникам
        Updated?.Invoke(this, EventArgs.Empty);
    }
}

class SettingsForm
{
    public void OnWindowUpdated(object sender, EventArgs e)
    {
        Console.WriteLine("SettingsForm реагує на оновлення вікна");
    }
}

Розберімо покроково:


var window = new Window();
var settingsForm = new SettingsForm();

window.Updated += settingsForm.OnWindowUpdated;

window.SimulateUpdate(); // SettingsForm реагує

// Користувач закрив форму. Ми втрачаємо всі посилання на неї:
settingsForm = null;

// Але об'єкт SettingsForm НЕ буде видалений збирачем сміття, доки window живий,
// тому що window.Updated все ще зберігає посилання на метод OnWindowUpdated,
// а значить, і на сам об'єкт SettingsForm.

Що робити?
Відписатися:


// Щоб це зробити, треба, щоб у нас лишилося посилання на обробник або об'єкт:
window.Updated -= settingsForm.OnWindowUpdated;
settingsForm = null; // Тепер об'єкт зможе бути видалений

Таблиця: хто тримає посилання на кого

Дія Хто зберігає посилання Чи можна звільнити памʼять?
Підписка на подію (+=) Видавець на підписника Ні, доки живий видавець
Відписка (-=) Ні Так, після видалення всіх зовнішніх посилань
Без підписки Ні Так

3. Як правильно організувати відписку

Явне видалення обробника

Це можна зробити, наприклад, у момент закриття вікна або форми:


class SettingsForm
{
    private readonly Window _window;

    public SettingsForm(Window window)
    {
        _window = window;
        _window.Updated += OnWindowUpdated;
    }

    public void Close()
    {
        _window.Updated -= OnWindowUpdated; // відписуємося!
        // Тут код для закриття (наприклад, Dispose, GC.SuppressFinalize, тощо)
    }

    public void OnWindowUpdated(object sender, EventArgs e)
    {
        // Обробка подій
    }
}

Якщо SettingsForm знищується за кнопкою «Закрити», важливо не забути викликати метод, де відбувається відписка (наприклад, Close()).

Використання інтерфейсу IDisposable

Для складних об’єктів, які підписуються на події та контролюють свій життєвий цикл, зручно реалізувати інтерфейс IDisposable. У методі Dispose() виконують усі потрібні відписки.


class SettingsForm : IDisposable
{
    private readonly Window _window;

    public SettingsForm(Window window)
    {
        _window = window;
        _window.Updated += OnWindowUpdated;
    }

    public void OnWindowUpdated(object sender, EventArgs e)
    {
        // ...
    }

    public void Dispose()
    {
        _window.Updated -= OnWindowUpdated;
        // Тут також звільняємо інші ресурси
    }
}

Тепер SettingsForm можна використовувати всередині блоку using, явно викликати Dispose() або автоматизувати звільнення ресурсів (наприклад, через GC.SuppressFinalize у типах із фіналізатором).

4. Робота з лямбда-виразами: підводні камені та лайфхаки

Якщо ви підписуєтеся на подію за допомогою лямбда-виразу, але не зберігаєте лямбду в змінну, ви не зможете відписатися.


// Підписка — анонімна лямбда
window.Updated += (s, e) => Console.WriteLine("Лямбда викликана!");

// Як тепер відписатися? — Ніяк!
window.Updated -= (s, e) => Console.WriteLine("Лямбда викликана!"); // Це інший делегат!

Що робити?
Збережіть лямбду в змінну-делегат:


EventHandler handler = (s, e) => Console.WriteLine("Лямбда викликана!");
window.Updated += handler;

// ... тепер можна відписатися!
window.Updated -= handler;

5. Корисні нюанси

Особливості життєвого циклу об’єктів і подій

Ще одна часта проблема — перехресні посилання через події між двома «довгоживучими» об’єктами. Наприклад, одне вікно підписане на подію іншого, обидва активно використовуються, не видаляються — і памʼять витрачається дедалі більше.

Рекомендація: намагайтеся завжди пам’ятати, хто на кого підписується і коли треба відписуватися. Якщо підписка має той самий життєвий цикл, що й видавець — це нормально. Якщо підписник може жити коротше за видавця, зробіть явну відписку.

Універсальне правило: «Якщо підписалися — відпишіться!»

  • Для довгоживучих видавців (наприклад, глобальних, сінглтонів, основних вікон) — завжди реалізуйте відписку у підписниках.
  • Для тимчасових об’єктів (наприклад, одноразових сповіщень або подій, де підписник живе довше за видавця) — можна не перейматися, але все одно стежте за контекстом.
  • Використовуйте підходи на кшталт WeakEvent (слабкі події) або спеціальні фреймворки, якщо не бажаєте керувати відпискою вручну.

6. Типові помилки при роботі з відпискою

Невдала відписка: метод-обробник має бути тим самим

Дуже важливо, щоб під час відписки ви вказували той самий обробник, що й під час підписки. Інакше відписка не спрацює.

Помилка:


window.Updated += settingsForm.OnWindowUpdated;
// ...
window.Updated -= new SettingsForm().OnWindowUpdated; // Не спрацює! Це інший екземпляр об'єкта й інший делегат!

Правильно:


window.Updated -= settingsForm.OnWindowUpdated;

Якщо під час підписки використовується анонімна лямбда без збереження посилання на делегат, відписатися від неї неможливо, бо це вже буде інший екземпляр делегата:


// Підписка
window.Updated += (s, e) => Console.WriteLine("Лямбда!");

// Спроба відписки — не спрацює!
window.Updated -= (s, e) => Console.WriteLine("Лямбда!");

«Забута» відписка

Дуже часто про відписку просто забувають, особливо коли підписник живе довше за видавця або коли розробник не до кінця розуміє механіку роботи подій. У результаті об’єкти-підписники залишаються в памʼяті довше, ніж потрібно, що призводить до витоків памʼяті та проблем із продуктивністю.

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