Нужно ли явно удалять обработчики событий в C #

120

У меня есть класс, который предлагает несколько мероприятий. Этот класс объявляется глобально, но не создается экземпляром этого глобального объявления - он создается по мере необходимости в методах, которые в нем нуждаются.

Каждый раз, когда этот класс требуется в методе, создается его экземпляр и регистрируются обработчики событий. Нужно ли явно удалять обработчики событий, прежде чем метод выйдет за пределы области видимости?

Когда метод выходит за пределы области видимости, уходит и экземпляр класса. Влияет ли оставление обработчиков событий, зарегистрированных в этом экземпляре, выходящем за пределы области видимости, на объем памяти? (Мне интересно, не позволяет ли обработчик событий GC видеть, что экземпляр класса больше не упоминается.)

гр.
источник

Ответы:

184

В твоем случае все нормально. Это объект, который публикует события, который поддерживает цели обработчиков событий. Итак, если у меня есть:

publisher.SomeEvent += target.DoSomething;

then publisherимеет ссылку, targetно не наоборот.

В вашем случае издатель будет иметь право на сборку мусора (при условии, что на него нет других ссылок), поэтому тот факт, что он получил ссылку на цели обработчика событий, не имеет значения.

Сложный случай - это когда издатель долгожитель, а подписчики не хотят им быть - в этом случае вам нужно отписаться от обработчиков. Например, предположим, что у вас есть служба передачи данных, которая позволяет подписаться на асинхронные уведомления об изменениях пропускной способности, а объект службы передачи является долгоживущим. Если мы сделаем это:

BandwidthUI ui = new BandwidthUI();
transferService.BandwidthChanged += ui.HandleBandwidthChange;
// Suppose this blocks until the transfer is complete
transferService.Transfer(source, destination);
// We now have to unsusbcribe from the event
transferService.BandwidthChanged -= ui.HandleBandwidthChange;

(На самом деле вы бы захотели использовать блок finally, чтобы не допустить утечки обработчика событий.) Если бы мы не отказались от подписки, то BandwidthUIони просуществовали бы как минимум столько же, сколько и служба передачи.

Лично я редко сталкиваюсь с этим - обычно, если я подписываюсь на событие, цель этого события живет, по крайней мере, столько же, сколько издатель - например, форма будет длиться столько же, сколько и кнопка, которая на ней. Об этой потенциальной проблеме стоит знать, но я думаю, что некоторые люди беспокоятся об этом, когда в этом нет необходимости, потому что они не знают, в какую сторону идут ссылки.

РЕДАКТИРОВАТЬ: это ответ на комментарий Джонатана Дикинсона. Во-первых, посмотрите документацию для Delegate.Equals (object), которая явно дает поведение равенства.

Во-вторых, вот короткая, но полная программа, показывающая, как работает отмена подписки:

using System;

public class Publisher
{
    public event EventHandler Foo;

    public void RaiseFoo()
    {
        Console.WriteLine("Raising Foo");
        EventHandler handler = Foo;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
        else
        {
            Console.WriteLine("No handlers");
        }
    }
}

public class Subscriber
{
    public void FooHandler(object sender, EventArgs e)
    {
        Console.WriteLine("Subscriber.FooHandler()");
    }
}

public class Test
{
    static void Main()
    {
         Publisher publisher = new Publisher();
         Subscriber subscriber = new Subscriber();
         publisher.Foo += subscriber.FooHandler;
         publisher.RaiseFoo();
         publisher.Foo -= subscriber.FooHandler;
         publisher.RaiseFoo();
    }
}

Полученные результаты:

Raising Foo
Subscriber.FooHandler()
Raising Foo
No handlers

(Проверено на Mono и .NET 3.5SP1.)

Дальнейшее редактирование:

Это должно доказать, что издателя событий можно собрать, пока есть ссылки на подписчика.

using System;

public class Publisher
{
    ~Publisher()
    {
        Console.WriteLine("~Publisher");
        Console.WriteLine("Foo==null ? {0}", Foo == null);
    }

    public event EventHandler Foo;
}

public class Subscriber
{
    ~Subscriber()
    {
        Console.WriteLine("~Subscriber");
    }

    public void FooHandler(object sender, EventArgs e) {}
}

public class Test
{
    static void Main()
    {
         Publisher publisher = new Publisher();
         Subscriber subscriber = new Subscriber();
         publisher.Foo += subscriber.FooHandler;

         Console.WriteLine("No more refs to publisher, "
             + "but subscriber is alive");
         GC.Collect();
         GC.WaitForPendingFinalizers();         

         Console.WriteLine("End of Main method. Subscriber is about to "
             + "become eligible for collection");
         GC.KeepAlive(subscriber);
    }
}

Результаты (в .NET 3.5SP1; здесь Mono ведет себя немного странно. Мы рассмотрим это как-нибудь):

No more refs to publisher, but subscriber is alive
~Publisher
Foo==null ? False
End of Main method. Subscriber is about to become eligible for collection
~Subscriber
Джон Скит
источник
2
Я согласен с этим, но, если возможно, вы можете вкратце уточнить или, желательно, сослаться на пример того, что вы имеете в виду под «но подписчики не хотят быть»?
Питер МакДжи,
@Jon: Я очень признателен, это не обычное явление, но, как вы говорите, я видел, как люди беспокоились об этом без необходимости.
Питер МакДжи,
- = Не работает. - = В результате будет создан новый делегат, и делегаты не проверяют равенство с помощью целевого метода, они выполняют object.ReferenceEquals () для делегата. Нового делегата нет в списке: он не действует (и, как ни странно, не вызывает ошибки).
Джонатан С. Дикинсон
2
@Jonathan: Нет, делегаты проверяют равенство с помощью целевого метода. Докажу в редактировании.
Джон Скит,
Я уступаю. Меня запутали анонимные делегаты.
Джонатан Дикинсон,
8

В вашем случае все в порядке. Изначально я прочитал ваш вопрос задом наперед, что подписчик выходит за рамки, а не издатель . Если издатель события выходит за пределы области видимости, то ссылки на подписчика (а не на самого подписчика, конечно!) Идут вместе с ним, и нет необходимости явно их удалять.

Мой исходный ответ ниже о том, что произойдет, если вы создадите подписчика на событие и отпустите его, не отписавшись от подписки. Это не относится к вашему вопросу, но я оставлю его для истории.

Если класс все еще зарегистрирован с помощью обработчиков событий, то он все еще доступен. Это все еще живой объект. ГХ, следящий за графом событий, обнаружит, что он подключен. Да, вы захотите явно удалить обработчики событий.

Тот факт, что объект находится вне области его первоначального распределения, не означает, что он является кандидатом на сборку мусора. Пока остается живая ссылка, она живая.

Эдди
источник
1
Я не считаю, что здесь нужна какая-либо отмена подписки - GC видит ссылки от издателя события, а не на него, и это издатель, который нас беспокоит здесь.
Джон Скит,
@Jon Skeet: Ты прав. Я прочитал вопрос задом наперед. Я исправил свой ответ, чтобы отразить реальность.
Эдди,