Как правильно подписаться на событие и отписаться от него в C#?

Ответ

Подписка на событие — это добавление метода-обработчика в список вызовов события. Делается это с помощью оператора +=. Отписка (критически важная для избежания утечек памяти) — с помощью оператора -=.

Базовый пример

public class Button
{
    public event EventHandler Clicked; // Объявление события

    public void SimulateClick()
    {
        Clicked?.Invoke(this, EventArgs.Empty); // Вызов события
    }
}

// Где-то в коде:
Button myButton = new Button();

// 1. Подписка с помощью именованного метода
myButton.Clicked += OnButtonClicked;

// 2. Отписка (например, при закрытии формы)
myButton.Clicked -= OnButtonClicked;

void OnButtonClicked(object sender, EventArgs e)
{
    Console.WriteLine("Кнопка была нажата.");
}

Использование лямбда-выражений и анонимных методов

Удобно для коротких обработчиков, но требует сохранения ссылки для отписки.

// Подписка с помощью лямбда-выражения
EventHandler handler = (s, e) => Console.WriteLine("Лямбда-обработчик!");
myButton.Clicked += handler;

// Для отписки нужна ссылка на ту же самую лямбду
myButton.Clicked -= handler;

Ключевые практики и подводные камни

  1. Всегда отписывайтесь. Если объект, содержащий метод-обработчик, живет дольше, чем объект-источник события, на который он подписан, сборщик мусора не сможет удалить источник. Это классическая утечка памяти. Отписывайтесь в методах Dispose, OnClosing и т.д.
  2. Потокобезопасность. Операции += и -= по умолчанию не являются атомарными. В многопоточных сценариях при одновременной подписке/отписке может возникнуть состояние гонки. Для потокобезопасной реализации событий используйте ключевое слово event с аксессорами add/remove и примитивами синхронизации (например, lock).
  3. Проверка на null. Всегда используйте null-условный оператор (?.) при вызове события, как показано в примере SimulateClick(). Это защищает от NullReferenceException, если на событие нет подписчиков.
  4. Использование слабых ссылок. В сложных сценариях (например, плагины) для избежания утечек памяти можно использовать шаблон слабой ссылки (WeakReference или WeakEventManager в WPF), но это требует дополнительной инфраструктуры.

Ответ 18+ 🔞

Слушай, а вот эти ваши события в C# — это ж, блядь, как кнопка на пульте от телевизора, только в коде. Вроде просто: нажал — работает, отпустил — перестало. Но если руки кривые, то можно так наворотить, что потом весь проект соснёшь.

Вот смотри, объявляешь ты событие, типа «кликнули по кнопке»:

public class Button
{
    public event EventHandler Clicked; // Вот оно, событие, висит как недоделанное
}

А чтобы его вызвать, когда кнопку ткнули, пишешь:

public void SimulateClick()
{
    Clicked?.Invoke(this, EventArgs.Empty); // Без этого "?." можно налететь на NullReferenceException, если никто не подписан
}

Теперь самое интересное — подписка. Это как договориться с соседом: «Мужик, если у меня кнопка сработает, я тебе скажу». Делается через +=.

Button myButton = new Button();
myButton.Clicked += OnButtonClicked; // Подписались, типа «окей, я в теме»

void OnButtonClicked(object sender, EventArgs e)
{
    Console.WriteLine("Кнопка была нажата, ёпта!");
}

А когда сосед съезжает или ты с ним поссорился — надо отписаться. -=, и всё, как будто и не знали друг друга.

myButton.Clicked -= OnButtonClicked; // Всё, свободен, Вася

А теперь, блядь, главное — ОТПИСЫВАЙСЯ, СУКА! Серьёзно. Если твой объект живёт вечно (как главная форма), а он подписан на событие какого-нибудь временного говнокомпонента, то этот компонент будет висеть в памяти до скончания времён. Сборщик мусора посмотрит на эту связь и такой: «Ну, раз вы друзья, то я вас не трону». И память течёт, как пиво из дырявой кружки. Отписывайся в Dispose, в деструкторе, в методе закрытия окна — где угодно, но делай это.

Ещё можно подписываться лямбдами, это удобно для мелких действий:

EventHandler handler = (s, e) => Console.WriteLine("Лямбда-обработчик, быстренько!");
myButton.Clicked += handler;
// ...
myButton.Clicked -= handler; // Отписаться — и всё, только ссылку на ту же лямбду надо сохранить

А теперь подводные камни, ёб твою мать:

  1. Потокобезопасность, её нет. Операции += и -= — не атомарные. Если из десяти потоков одновременно начать подписываться и отписываться, можно получить такое состояние гонки, что мозг сломаешь. Для серьёзных вещей нужно городить lock или использовать специальные потокобезопасные коллекции.

  2. Проверка на null — святое. Всегда пиши Clicked?.Invoke(...). Иначе, если подписчиков нет, вылетит исключение, и будет тебе, как говорится, хиросима.

  3. Слабые ссылки — для продвинутых. Если совсем запара с утечками памяти (например, пишешь систему плагинов), можно использовать WeakReference. Но это уже высший пилотаж, там свои грабли.

Короче, события — мощная хуйня, но если использовать их без мозгов, можно так прострелить себе ногу, что потом полгода на костылях ходить. Отписывайся, проверяй на null и думай головой, а не жопой.