Чи потрібно явно видалити обробники подій у C #


120

У мене є клас, який пропонує кілька подій. Цей клас оголошується у всьому світі, але не встановлюється на цій глобальній декларації - він встановлюється за потребою в методах, які потребують цього.

Кожен раз, коли цей клас потрібен методу, він інсталюється та реєструються обробники подій. Чи потрібно явно видаляти обробники подій, перш ніж метод вийде з сфери застосування?

Коли метод виходить за межі області, так виходить і екземпляр класу. Чи залишає обробники подій, зареєстрованих у тому екземплярі, який виходить із сфери застосування, наслідком пам'яті? (Мені цікаво, чи обробник подій перешкоджає GC бачити екземпляр класу як більше не посилається.)

Відповіді:


184

У вашому випадку все добре. Це об'єкт, який публікує події, який зберігає цілі обробників подій. Тож якщо я маю:

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;

(Ви насправді хочете використати остаточний блок, щоб переконатися, що ви не просочилися обробником події.) Якщо ми не скасували підписку, то програма BandwidthUIпрожила б хоча б стільки, скільки послуга передачі.

Особисто я рідко стикаюся з цим - зазвичай, якщо я підписуюся на подію, ціль цієї події живе принаймні стільки, скільки видавець, - форма триватиме так довго, як, наприклад, кнопка, яка знаходиться на ній. Варто знати про це потенційне питання, але я думаю, що деякі люди турбуються про це, коли вони не потребують, тому що вони не знають, в який бік ідуть посилання.

РЕДАКТ: Це відповідь на коментар Джонатана Дікінсона. По-перше, подивіться на документи для Delegate.Equals (об'єкт), які чітко дають поведінку рівності.

По-друге, ось коротка, але повна програма, яка показує роботу підписки:

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; Моно, як видається, поводиться тут трохи дивно. Це буде розглянуто деякий час):

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: Дуже вдячний, це не є звичайним явищем, але, як ви кажете, я бачив, як люди непотрібно турбуються про це.
Пітер Макг

- = Не працює. - = Виведе новий делегат, і делегати не перевірять рівність за допомогою цільового методу, вони роблять об'єкт.ReferenceEquals () на делегата. Нового делегата в списку не існує: він не має ефекту (і не видає помилку як не дивно).
Джонатан С Дікінсон

2
@Jonathan: Ні, делегати перевіряють рівність за допомогою цільового методу. Доведемо в редакції.
Джон Скіт

Я поступаюсь. Я плутався з анонімними делегатами.
Джонатан C Дікінсон

8

У вашому випадку у вас все добре. Я спочатку читав ваше запитання назад, що передплатник вийшов за межі сфери, а не видавець . Якщо видавець події виходить за межі сфери, то посилання на підписника (звичайно, не самого підписника!) Йдуть разом із ним, і немає необхідності їх явно видаляти.

Моя оригінальна відповідь нижче - про те, що станеться, якщо ви створили підписника події і нехай він виходить за межі без підписки Це не стосується вашого питання, але я залишу його на місці для історії.

Якщо клас все-таки реєструється через обробники подій, він все ще доступний. Це все ще живий об’єкт. GC, що слідує за графіком події, знайде його пов'язаним. Так, ви хочете явно видалити обробники подій.

Тільки тому, що об'єкт не входить у рамки його первісного виділення, це не означає, що він є кандидатом в ГК. Поки залишається пряма довідка, це живе.


1
Я не вважаю, що будь-яка підписка тут необхідна - GC бачить посилання від видавця події, а не на нього, і про нас видає турбота про видавця.
Джон Скіт

@Jon Skeet: Ти маєш рацію. Я читав питання назад. Я виправив свою відповідь, щоб відобразити реальність.
Едді
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.