← Все вопросы

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

Задан 15 месяцев назад755 просмотров2 ответа
7

Подписываюсь на событие лямбдой через +=, всё работает. Но прочитал, что если не отписываться, могут быть утечки памяти и обработчик будет вызываться лишний раз. Попробовал отписаться через -= — и обработчик всё равно срабатывает! Видимо, я что-то делаю не так с делегатами.

class Timer
{
    public event Action Tick;
    public void Fire() => Tick?.Invoke();
}

var timer = new Timer();
timer.Tick += () => Console.WriteLine("тик");
// ... позже хочу отписаться
timer.Tick -= () => Console.WriteLine("тик");  // не работает?!
timer.Fire();  // всё равно печатает "тик"

Почему -= не убирает подписку, если лямбда вроде бы такая же?

2 ответа

16
✓ Принятый ответ — помог автору

Проблема в том, что две одинаковые на вид лямбды — это два разных объекта-делегата. Когда ты пишешь () => ... второй раз, создаётся новый делегат, и -= ищет именно его в списке подписчиков, а там его нет. Старая подписка остаётся.

Чтобы отписаться, нужно сохранить ту же самую ссылку на делегат и использовать её и для +=, и для -=:

var timer = new Timer();

Action handler = () => Console.WriteLine("тик");
timer.Tick += handler;

// ... позже
timer.Tick -= handler;   // теперь реально отписались
timer.Fire();            // ничего не печатает

Правило: если планируешь отписываться — не используй анонимную лямбду прямо в +=, сохрани её в переменную или вынеси в именованный метод. Иначе ссылку для -= будет просто неоткуда взять.

6

Про утечки памяти, раз ты упомянул. Опасность в том, что пока объект-издатель события держит ссылку на делегат подписчика, подписчик не может быть собран сборщиком мусора — событие держит его живым. Если издатель долго живёт (например, статический или глобальный), а подписчиков много создаётся и «забывается», память течёт.

Поэтому именованный метод/сохранённая лямбда + явный -= в нужный момент (например, при закрытии формы или Dispose) — это не просто чистота, а защита от утечки:

class Subscriber : IDisposable
{
    private readonly Timer _timer;
    public Subscriber(Timer t) { _timer = t; _timer.Tick += OnTick; }
    private void OnTick() => Console.WriteLine("тик");
    public void Dispose() => _timer.Tick -= OnTick;
}

Именованный метод OnTick — это всегда одна и та же ссылка, так что -= гарантированно сработает.

Ваш ответ

Войдите, чтобы ответить на вопрос.
Поддержать проект