¿Cómo los eventos causan memory leaks en C # y cómo las referencias débiles ayudan a mitigar eso?

Hay dos formas (que yo conozco) de provocar una pérdida involuntaria de memoria en C #:

  1. No deshacerse de los recursos que implementan IDisposable
  2. Referenciar y quitar referencias de eventos incorrectamente.

Realmente no entiendo el segundo punto. Si el objeto fuente tiene una vida útil más larga que el oyente, y el oyente ya no necesita los eventos cuando no hay otras referencias a él, el uso de eventos .NET normales causa una pérdida de memoria: el objeto fuente mantiene objetos oyentes en la memoria que debería ser basura recolectada.

¿Puede explicar cómo los eventos pueden causar filtraciones de memoria con código en C # y cómo puedo codificar para evitarlo utilizando referencias débiles y sin referencias débiles?

Cuando un oyente conecta un oyente de evento a un evento, el objeto fuente obtendrá una referencia al objeto oyente. Esto significa que el recolector de elementos no utilizados no puede recostackr el elemento de escucha hasta que el controlador de eventos se separe o se recopile el objeto de origen.

Considere las siguientes clases:

 class Source { public event EventHandler SomeEvent; } class Listener { public Listener(Source source) { // attach an event listner; this adds a reference to the // source_SomeEvent method in this instance to the invocation list // of SomeEvent in source source.SomeEvent += new EventHandler(source_SomeEvent); } void source_SomeEvent(object sender, EventArgs e) { // whatever } } 

… y luego el siguiente código:

 Source newSource = new Source(); Listener listener = new Listener(newSource); listener = null; 

Aunque asignamos null al listener , no será elegible para la recolección de elementos no utilizados, ya que newSource todavía contiene una referencia al manejador de eventos ( Listener.source_SomeEvent ). Para solucionar este tipo de pérdida, es importante separar siempre los detectores de eventos cuando ya no los necesiten.

La muestra anterior está escrita para enfocarse en el problema con la fuga. Para arreglar ese código, lo más fácil será dejar que Listener retenga una referencia a Source , para luego poder desconectar el detector de eventos:

 class Listener { private Source _source; public Listener(Source source) { _source = source; // attach an event listner; this adds a reference to the // source_SomeEvent method in this instance to the invocation list // of SomeEvent in source _source.SomeEvent += source_SomeEvent; } void source_SomeEvent(object sender, EventArgs e) { // whatever } public void Close() { if (_source != null) { // detach event handler _source.SomeEvent -= source_SomeEvent; _source = null; } } } 

Entonces, el código de llamada puede indicar que se hace usando el objeto, lo que eliminará la referencia que Source tiene de’Listener`;

 Source newSource = new Source(); Listener listener = new Listener(newSource); // use listener listener.Close(); listener = null; 

Lea el excelente artículo de Jon Skeet sobre los eventos. No es una verdadera “fuga de memoria” en el sentido clásico, sino más bien una referencia retenida que no ha sido desconectada. Recuerde siempre -= un manejador de eventos que usted += en un punto anterior y usted debe ser dorado.

En sentido estricto, no hay “pérdidas de memoria” dentro de la “caja de arena” de un proyecto .NET administrado; solo hay referencias que se conservan más de lo que el desarrollador consideraría necesario. Fredrik tiene el derecho de eso; cuando adjuntas un manejador a un evento, porque el manejador es generalmente un método de instancia (que requiere la instancia), la instancia de la clase que contiene el oyente permanece en la memoria siempre que se mantenga esta referencia. Si la instancia del oyente contiene referencias a otras clases a su vez (por ejemplo, referencias a los objetos que contienen), el montón puede permanecer bastante grande mucho después de que el oyente haya salido de todos los demás ámbitos.

Tal vez alguien con un conocimiento un poco más esotérico de Delegate y MulticastDelegate pueda arrojar algo de luz sobre esto. De la forma en que lo veo, una PÉRDIDA verdadera PODRÍA ser posible si todo lo siguiente fuera verdad:

  • El detector de eventos requiere que se liberen recursos externos / no administrados mediante la implementación de IDisposable, pero no es así o
  • El delegado de multidifusión de eventos NO llama a los métodos de Dispose () de su método Finalize () anulado, y
  • La clase que contiene el evento no llama a Dispose () en cada Target del delegado a través de su propia implementación IDisposable, o en Finalize ().

Nunca he oído hablar de ninguna práctica recomendada que implique llamar a Dispose () en Delegate Targets, y mucho menos a los oyentes de eventos, por lo que solo puedo suponer que los desarrolladores de .NET sabían lo que estaban haciendo en este caso. Si esto es cierto, y el MulticastDelegate detrás de un evento trata de eliminar adecuadamente a los oyentes, entonces todo lo que se necesita es una implementación adecuada de IDisposable en una clase de escucha que requiere eliminación.