Czy konieczne jest jawne usunięcie programów obsługi zdarzeń w języku C #

120

Mam klasę, która oferuje kilka wydarzeń. Ta klasa jest zadeklarowana globalnie, ale nie jest instancją na tej globalnej deklaracji - jest ona instanowana w razie potrzeby w metodach, które tego potrzebują.

Za każdym razem, gdy klasa jest potrzebna w metodzie, jest ona instancją i rejestrowane są programy obsługi zdarzeń. Czy konieczne jest jawne usunięcie programów obsługi zdarzeń, zanim metoda wyjdzie poza zakres?

Kiedy metoda wychodzi poza zakres, tak samo dzieje się z instancją klasy. Czy pozostawienie programów obsługi zdarzeń zarejestrowanych w tym wystąpieniu, które wykracza poza zakres, ma wpływ na pamięć? (Zastanawiam się, czy program obsługi zdarzeń uniemożliwia GC zobaczenie wystąpienia klasy jako już nie przywoływanego).

rp.
źródło

Odpowiedzi:

184

W twoim przypadku wszystko jest w porządku. Jest to obiekt, który publikuje zdarzenia, który utrzymuje cele programów obsługi zdarzeń na żywo. Więc jeśli mam:

publisher.SomeEvent += target.DoSomething;

to publisherma odniesienie dotarget ale nie odwrotnie.

W twoim przypadku wydawca będzie kwalifikował się do wyrzucania elementów bezużytecznych (zakładając, że nie ma do niego innych odniesień), więc fakt, że ma odniesienie do celów obsługi zdarzeń, jest nieistotny.

Trudny przypadek polega na tym, że wydawca jest długowieczny, ale subskrybenci nie chcą - w takim przypadku musisz zrezygnować z subskrypcji programów obsługi. Na przykład załóżmy, że masz usługę przesyłania danych, która umożliwia subskrybowanie asynchronicznych powiadomień o zmianach przepustowości, a obiekt usługi transferu jest długotrwały. Jeśli to zrobimy:

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;

(Właściwie chciałbyś użyć bloku last, aby upewnić się, że nie wyciekniesz z modułu obsługi zdarzeń.) Jeśli nie anulowaliśmy subskrypcji, to BandwidthUI działałby przynajmniej tak długo, jak usługa transferu.

Osobiście rzadko się z tym spotykam - zazwyczaj jeśli zapiszę się na wydarzenie, cel tego wydarzenia żyje co najmniej tak długo, jak wydawca - formularz będzie trwał tak długo, jak na przykład przycisk, który na nim jest. Warto wiedzieć o tym potencjalnym problemie, ale myślę, że niektórzy martwią się tym, kiedy nie potrzebują, ponieważ nie wiedzą, w którą stronę prowadzą odniesienia.

EDYCJA: To jest odpowiedź na komentarz Jonathana Dickinsona. Po pierwsze, spójrz na dokumentację Delegate.Equals (obiekt) która jasno przedstawia zachowanie równości.

Po drugie, oto krótki, ale kompletny program pokazujący działanie anulowania subskrypcji:

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();
    }
}

Wyniki:

Raising Foo
Subscriber.FooHandler()
Raising Foo
No handlers

(Testowane na Mono i .NET 3.5SP1.)

Dalsza edycja:

Ma to na celu udowodnienie, że wydawcę wydarzeń można zebrać, dopóki istnieją odniesienia do subskrybenta.

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);
    }
}

Wyniki (w .NET 3.5SP1; Mono wydaje się tutaj zachowywać nieco dziwnie. Przyjrzymy się temu kiedyś):

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
Jon Skeet
źródło
2
Zgadzam się z tym, ale jeśli to możliwe, czy możesz krótko rozwinąć lub najlepiej odnieść się do przykładu, co masz na myśli, mówiąc „ale subskrybenci nie chcą być”?
Peter McG,
@Jon: Bardzo doceniane, nie jest to częste, ale jak powiedziałeś, widziałem ludzi niepotrzebnie martwiących się o to.
Peter McG,
- = Nie działa. - = spowoduje utworzenie nowego delegata, a delegaci nie sprawdzają równości przy użyciu metody docelowej, wykonują obiekt.ReferenceEquals () na delegacie. Nowy delegat nie istnieje na liście: nie ma żadnego efektu (i nie zgłasza wystarczająco dziwnego błędu).
Jonathan C Dickinson
2
@Jonathan: Nie, delegaci sprawdzają równość metodą docelową. Sprawdzi się w edycji.
Jon Skeet
Przyznaję. Pomyliłem się z anonimowymi delegatami.
Jonathan C Dickinson
8

W twoim przypadku wszystko w porządku. Początkowo przeczytałem twoje pytanie od tyłu, że subskrybent wykracza poza zakres, a nie wydawca . Jeśli wydawca wydarzenia wykracza poza zakres, odwołania wraz z nim idą do subskrybenta (oczywiście nie do samego subskrybenta!) I nie ma potrzeby ich jawnego usuwania.

Moja pierwotna odpowiedź jest poniżej, o tym, co się stanie, jeśli utworzysz subskrybenta wydarzenia i pozwolisz mu wyjść poza zakres bez anulowania subskrypcji. Nie dotyczy twojego pytania, ale zostawię je na liście historii.

Jeśli klasa jest nadal zarejestrowana za pośrednictwem programów obsługi zdarzeń, nadal jest osiągalna. To wciąż żywy obiekt. GC podążający za wykresem zdarzenia znajdzie połączenie. Tak, będziesz chciał jawnie usunąć programy obsługi zdarzeń.

To, że obiekt jest poza zakresem jego pierwotnej alokacji, nie oznacza, że ​​jest kandydatem do GC. Dopóki pozostaje odniesienie na żywo, jest aktywne.

Eddie
źródło
1
Nie sądzę, aby jakakolwiek rezygnacja z subskrypcji była tutaj konieczna - GC widzi odniesienia od wydawcy wydarzenia, a nie do niego, i to wydawcy się tutaj martwimy.
Jon Skeet
@Jon Skeet: Masz rację. Przeczytałem pytanie od tyłu. Poprawiłem odpowiedź, aby odzwierciedlić rzeczywistość.
Eddie,