Как отписаться от события в C#, чтобы лямбда не висела в памяти?
Подписываюсь на событие лямбдой через +=, всё работает. Но прочитал, что если не отписываться, могут быть утечки памяти и обработчик будет вызываться лишний раз. Попробовал отписаться через -= — и обработчик всё равно срабатывает! Видимо, я что-то делаю не так с делегатами.
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 ответа
Проблема в том, что две одинаковые на вид лямбды — это два разных объекта-делегата. Когда ты пишешь () => ... второй раз, создаётся новый делегат, и -= ищет именно его в списке подписчиков, а там его нет. Старая подписка остаётся.
Чтобы отписаться, нужно сохранить ту же самую ссылку на делегат и использовать её и для +=, и для -=:
var timer = new Timer();
Action handler = () => Console.WriteLine("тик");
timer.Tick += handler;
// ... позже
timer.Tick -= handler; // теперь реально отписались
timer.Fire(); // ничего не печатает
Правило: если планируешь отписываться — не используй анонимную лямбду прямо в +=, сохрани её в переменную или вынеси в именованный метод. Иначе ссылку для -= будет просто неоткуда взять.
Про утечки памяти, раз ты упомянул. Опасность в том, что пока объект-издатель события держит ссылку на делегат подписчика, подписчик не может быть собран сборщиком мусора — событие держит его живым. Если издатель долго живёт (например, статический или глобальный), а подписчиков много создаётся и «забывается», память течёт.
Поэтому именованный метод/сохранённая лямбда + явный -= в нужный момент (например, при закрытии формы или 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 — это всегда одна и та же ссылка, так что -= гарантированно сработает.