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

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

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

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

Ответ 1

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

publisher.SomeEvent += target.DoSomething;

то 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 будет жить как минимум до тех пор, пока служба передачи.

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

EDIT: Это ответ на комментарий Джонатана Дикинсона. Во-первых, рассмотрите документы для 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

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

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

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

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