È necessario rimuovere esplicitamente i gestori di eventi in C #


120

Ho una classe che offre alcuni eventi. Quella classe è dichiarata globalmente ma non istanziata su quella dichiarazione globale: viene istanziata in base alle necessità nei metodi che ne hanno bisogno.

Ogni volta che quella classe è necessaria in un metodo, viene istanziato e i gestori di eventi vengono registrati. È necessario rimuovere esplicitamente i gestori di eventi prima che il metodo esca dall'ambito?

Quando il metodo esce dall'ambito, lo stesso vale per l'istanza della classe. L'abbandono dei gestori di eventi registrati con quell'istanza che esce dall'ambito ha un'implicazione sull'impronta di memoria? (Mi chiedo se il gestore di eventi impedisce al GC di vedere l'istanza della classe come non più referenziata.)

Risposte:


184

Nel tuo caso, va tutto bene. È l'oggetto che pubblica gli eventi che mantiene in vita gli obiettivi dei gestori di eventi. Quindi se ho:

publisher.SomeEvent += target.DoSomething;

quindi publisherha un riferimento a targetma non viceversa.

Nel tuo caso, l'editore sarà idoneo per la garbage collection (supponendo che non ci siano altri riferimenti ad esso), quindi il fatto che abbia un riferimento alle destinazioni del gestore di eventi è irrilevante.

Il caso complicato è quando l'editore è longevo ma gli abbonati non vogliono esserlo: in tal caso è necessario annullare l'iscrizione ai gestori. Ad esempio, supponi di avere un servizio di trasferimento dati che ti consente di iscriverti a notifiche asincrone sulle modifiche della larghezza di banda e che l'oggetto del servizio di trasferimento sia di lunga durata. Se lo facciamo:

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;

(In realtà vorresti usare un blocco finalmente per assicurarti di non perdere il gestore di eventi.) Se non cancellassimo l'iscrizione, allora BandwidthUIvivrebbe almeno quanto il servizio di trasferimento.

Personalmente mi imbatto raramente in questo - di solito se mi iscrivo a un evento, il target di quell'evento vive almeno quanto l'editore - un modulo durerà quanto il pulsante che si trova su di esso, per esempio. Vale la pena conoscere questo potenziale problema, ma penso che alcune persone se ne preoccupano quando non ne hanno bisogno, perché non sanno da che parte vanno i riferimenti.

EDIT: Questo è per rispondere al commento di Jonathan Dickinson. In primo luogo, guarda i documenti per Delegate.Equals (oggetto) che danno chiaramente il comportamento di uguaglianza.

In secondo luogo, ecco un programma breve ma completo per mostrare il funzionamento dell'annullamento dell'iscrizione:

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

risultati:

Raising Foo
Subscriber.FooHandler()
Raising Foo
No handlers

(Testato su Mono e .NET 3.5SP1.)

Ulteriore modifica:

Questo per dimostrare che un editore di eventi può essere raccolto mentre ci sono ancora riferimenti a un abbonato.

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

Risultati (in .NET 3.5SP1; Mono sembra comportarsi in modo leggermente strano qui. Lo esamineremo un po 'di tempo):

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
Sono d'accordo con questo, ma se possibile puoi approfondire brevemente o preferibilmente fare riferimento a un esempio di ciò che intendi con "ma gli abbonati non vogliono essere"?
Peter McG

@ Jon: Molto apprezzato, non è comune ma come dici tu ho visto persone preoccuparsi di questo inutilmente.
Peter McG,

- = Non funziona. - = Risulterà in un nuovo delegato ei delegati non controllano l'uguaglianza utilizzando il metodo di destinazione, ma eseguono un object.ReferenceEquals () sul delegato. Il nuovo delegato non esiste nell'elenco: non ha effetto (e non genera un errore abbastanza stranamente).
Jonathan C Dickinson,

2
@ Jonathan: No, i delegati controllano l'uguaglianza utilizzando il metodo target. Si dimostrerà in una modifica.
Jon Skeet

Lo ammetto. Mi sono confuso con delegati anonimi.
Jonathan C Dickinson,

8

Nel tuo caso, stai bene. Inizialmente ho letto la tua domanda al contrario, che un abbonato stava uscendo dall'ambito, non l' editore . Se l'editore dell'evento esce dall'ambito, i riferimenti al sottoscrittore (non all'abbonato stesso, ovviamente!) Vanno con esso e non è necessario rimuoverli esplicitamente.

La mia risposta originale è di seguito, su cosa succede se crei un abbonato a un evento e lo lasci uscire dall'ambito senza annullare l'iscrizione. Non si applica alla tua domanda, ma lo lascerò per la storia.

Se la classe è ancora registrata tramite gestori di eventi, è ancora raggiungibile. È ancora un oggetto vivo. Un GC che segue il grafico di un evento lo troverà connesso. Sì, vorrai rimuovere esplicitamente i gestori di eventi.

Solo perché l'oggetto è fuori dall'ambito della sua allocazione originale non significa che sia un candidato per GC. Finché rimane un riferimento live, è live.


1
Non credo che qui sia necessario annullare l'iscrizione: il GC vede riferimenti dall'editore dell'evento, non ad esso, ed è l'editore di cui siamo preoccupati qui.
Jon Skeet

@ Jon Skeet: hai ragione. Ho letto la domanda al contrario. Ho corretto la mia risposta per riflettere la realtà.
Eddie
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.