¿Es necesario eliminar explícitamente los controladores de eventos en C #?

120

Tengo una clase que ofrece algunos eventos. Esa clase se declara globalmente pero no se instancia en esa declaración global; se instancia según sea necesario en los métodos que la necesitan.

Cada vez que se necesita esa clase en un método, se crea una instancia y se registran los controladores de eventos. ¿Es necesario eliminar los controladores de eventos explícitamente antes de que el método salga del alcance?

Cuando el método sale del alcance, también lo hace la instancia de la clase. ¿Dejar los controladores de eventos registrados con esa instancia que está saliendo del alcance tiene una implicación de huella de memoria? (Me pregunto si el controlador de eventos evita que el GC vea que la instancia de clase ya no está referenciada).

rp.
fuente

Respuestas:

184

En tu caso, todo está bien. Es el objeto que publica los eventos que mantiene activos los objetivos de los controladores de eventos. Entonces, si tengo:

publisher.SomeEvent += target.DoSomething;

luego publishertiene una referencia a, targetpero no al revés.

En su caso, el editor será elegible para la recolección de basura (suponiendo que no haya otras referencias a él), por lo que el hecho de que tenga una referencia a los destinos del controlador de eventos es irrelevante.

El caso complicado es cuando el editor tiene una larga vida, pero los suscriptores no quieren serlo; en ese caso, debe cancelar la suscripción de los controladores. Por ejemplo, suponga que tiene algún servicio de transferencia de datos que le permite suscribirse a notificaciones asincrónicas sobre cambios de ancho de banda, y el objeto de servicio de transferencia es de larga duración. Si hacemos esto:

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;

(En realidad, querría usar un bloque finalmente para asegurarse de no filtrar el controlador de eventos). Si no cancelamos la suscripción, entonces el servicio BandwidthUIpermanecería al menos tanto tiempo como el servicio de transferencia.

Personalmente, rara vez me encuentro con esto, por lo general, si me suscribo a un evento, el objetivo de ese evento vive al menos tanto como el editor, un formulario durará tanto como el botón que está en él, por ejemplo. Vale la pena conocer este problema potencial, pero creo que algunas personas se preocupan por él cuando no es necesario, porque no saben hacia dónde van las referencias.

EDITAR: Esto es para responder al comentario de Jonathan Dickinson. En primer lugar, mire los documentos de Delegate.Equals (objeto) que claramente dan el comportamiento de igualdad.

En segundo lugar, aquí hay un programa breve pero completo para mostrar que la cancelación de la suscripción funciona:

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

Resultados:

Raising Foo
Subscriber.FooHandler()
Raising Foo
No handlers

(Probado en Mono y .NET 3.5SP1.)

Edición adicional:

Esto es para demostrar que se puede recopilar un editor de eventos mientras todavía hay referencias a un suscriptor.

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

Resultados (en .NET 3.5SP1; Mono parece comportarse de manera un poco extraña aquí. Lo veremos en algún momento):

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
fuente
2
Estoy de acuerdo con esto, pero si es posible, ¿puede explicar brevemente o preferiblemente referirse a un ejemplo de lo que quiere decir con "pero los suscriptores no quieren ser"?
Peter McG
@Jon: Muy apreciado, no es común, pero como dices, he visto a gente preocuparse por esto innecesariamente.
Peter McG
- = No funciona. - = Dará como resultado un nuevo delegado, y los delegados no verifican la igualdad usando el método objetivo, hacen un object.ReferenceEquals () en el delegado. El nuevo delegado no existe en la lista: no tiene ningún efecto (y, curiosamente, no arroja un error).
Jonathan C Dickinson
2
@Jonathan: No, los delegados verifican la igualdad usando el método objetivo. Probará en una edición.
Jon Skeet
Concedo. Me confundí con delegados anónimos.
Jonathan C Dickinson
8

En tu caso, estás bien. Originalmente leí su pregunta al revés, que un suscriptor estaba saliendo del alcance, no el editor . Si el editor del evento sale del alcance, entonces las referencias al suscriptor (¡no al suscriptor en sí, por supuesto!) Lo acompañan y no hay necesidad de eliminarlas explícitamente.

Mi respuesta original está a continuación, sobre lo que sucede si crea un suscriptor de evento y lo deja fuera de alcance sin darse de baja. No se aplica a su pregunta, pero lo dejaré para la historia.

Si la clase todavía está registrada a través de controladores de eventos, todavía es accesible. Sigue siendo un objeto vivo. Un GC que siga un gráfico de eventos lo encontrará conectado. Sí, querrá eliminar explícitamente los controladores de eventos.

El hecho de que el objeto esté fuera del alcance de su asignación original no significa que sea candidato para GC. Mientras permanezca una referencia en vivo, es en vivo.

Eddie
fuente
1
No creo que sea necesario cancelar la suscripción aquí: el GC ve referencias del editor del evento, no a él, y es el editor lo que nos preocupa aquí.
Jon Skeet
@Jon Skeet: Tienes razón. Leí la pregunta al revés. He corregido mi respuesta para reflejar la realidad.
Eddie