Usando IDisposable para cancelar la suscripción a eventos

Tengo una clase que maneja eventos desde un control WinForms. En función de lo que esté haciendo el usuario, estoy designando una instancia de la clase y creando una nueva para manejar el mismo evento. Necesito cancelar la suscripción a la instancia anterior del evento primero, lo suficientemente fácil. Me gustaría hacer esto de manera no propietaria si es posible, y parece que este es un trabajo para IDisposable. Sin embargo, la mayoría de la documentación recomienda IDisposable solo cuando se usan recursos no administrados, que no se aplica aquí.

Si implemento IDisposable y me cancelo la suscripción del evento en Dispose (), ¿estoy pervirtiendo su intención? ¿Debo, en cambio, proporcionar una función de cancelación de suscripción () y llamarla?


Editar: Aquí hay un código ficticio que muestra lo que estoy haciendo (usando IDisposable). Mi implementación real está relacionada con algún enlace de datos propietario (larga historia).

class EventListener : IDisposable { private TextBox m_textBox; public EventListener(TextBox textBox) { m_textBox = textBox; textBox.TextChanged += new EventHandler(textBox_TextChanged); } void textBox_TextChanged(object sender, EventArgs e) { // do something } public void Dispose() { m_textBox.TextChanged -= new EventHandler(textBox_TextChanged); } } class MyClass { EventListener m_eventListener = null; TextBox m_textBox = new TextBox(); void SetEventListener() { if (m_eventListener != null) m_eventListener.Dispose(); m_eventListener = new EventListener(m_textBox); } } 

En el código actual, la clase “EventListener” está más involucrada, y cada instancia tiene una importancia única. Los uso en una colección y los creo / destruyo a medida que el usuario hace clic.


Conclusión

Estoy aceptando la respuesta de gbjbaanb , al menos por ahora. Siento que el beneficio de usar una interfaz familiar supera cualquier posible inconveniente de usarlo donde no esté involucrado un código no administrado (¿cómo podría saberlo un usuario de este objeto?).

Si alguien no está de acuerdo, publique / comente / edite. Si se puede hacer una mejor discusión contra IDisposable, entonces cambiaré la respuesta aceptada.

Sí, ve por ello. Aunque algunas personas piensan que IDisposable se implementa solo para recursos no administrados, este no es el caso: los recursos no administrados son la mejor opción y la razón más obvia para implementarlo. Creo que adquirió esta idea porque las personas no podían pensar en ninguna otra razón para usarla. No es como un finalizador, que es un problema de rendimiento y no es fácil de manejar para el GC.

Ponga cualquier código de ordenación en su método de eliminación. Será más claro, más limpio y tendrá una mayor probabilidad de evitar memory leaks y una maldita vista más fácil de usar correctamente que intentar recordar deshacer sus referencias.

La intención de IDisposable es hacer que su código funcione mejor sin tener que hacer mucho trabajo manual. Usa su poder a tu favor y supera una tontería artificial de “intención de diseño”.

Recuerdo que ya era bastante difícil convencer a Microsoft de la utilidad de la finalización determinista cuando apareció .NET por primera vez: ganamos la batalla y los convencimos para que la agreguen (¡aunque solo fuera un patrón de diseño en ese momento), úsala!

Mi voto personal sería tener un método de cancelación de suscripción para eliminar la clase de los eventos. IDisposable es un patrón destinado a la liberación determinística de recursos no gestionados. En este caso, no administra ningún recurso no administrado y, por lo tanto, no debería implementar IDisposable.

IDisposable se puede usar para administrar suscripciones de eventos, pero probablemente no debería. Por ejemplo, te señalo a WPF. Esta es una biblioteca llena de eventos y controladores de eventos. Sin embargo, prácticamente ninguna clase en WPF implementa IDisposable. Lo tomaría como una indicación de que los eventos deberían ser manejados de otra manera.

Una cosa que me molesta sobre el uso de IDisposable patrón IDisposable para cancelar la suscripción a eventos es el problema de Finalización.

Dispose() en IDisposable se supone que debe ser llamada por el desarrollador, SIN EMBARGO, si el desarrollador no la llama, se entiende que el GC llamará a esta función (por el patrón de IDisposable estándar, al menos). En su caso, sin embargo, si no llama a Dispose nadie más lo hará – El evento permanece y la referencia fuerte impide que GC llame al finalizador.

El solo hecho de que Dispose () no sea llamado automáticamente por GC me parece suficiente como para no usar IDisposable en este caso. Quizás requiera una nueva interfaz específica de aplicación que diga que este tipo de objeto debe tener una función de limpieza llamada para ser eliminada por GC.

Creo que desechable es para cualquier cosa que GC no pueda encargarse automáticamente, y las referencias de eventos cuentan en mi libro. Aquí hay una clase de ayuda con la que se me ocurrió.

 public class DisposableEvent : IDisposable { EventHandler> Target { get; set; } public T Args { get; set; } bool fired = false; public DisposableEvent(EventHandler> target) { Target = target; Target += new EventHandler>(subscriber); } public bool Wait(int howLongSeconds) { DateTime start = DateTime.Now; while (!fired && (DateTime.Now - start).TotalSeconds < howLongSeconds) { Thread.Sleep(100); } return fired; } void subscriber(object sender, EventArgs e) { Args = e.Value; fired = true; } public void Dispose() { Target -= subscriber; Target = null; } } 

que te permite escribir este código:

 Class1 class1 = new Class1(); using (var x = new DisposableEvent(class1.Test)) { if (x.Wait(30)) { var result = x.Args; } } 

Un efecto colateral, no debes usar la palabra clave event en tus eventos, ya que eso evita pasarlos como un parámetro al constructor auxiliar, sin embargo, parece que no tiene ningún efecto negativo.

Por todo lo que leí sobre los desechables, diría que en realidad fueron inventados principalmente para resolver un problema: liberar los recursos del sistema no administrados de manera oportuna. Pero aún así todos los ejemplos que encontré no solo se centran en el tema de recursos no administrados, sino que también tienen otra propiedad en común: Dispose se llama solo para acelerar un proceso que de otro modo se hubiera producido más tarde automáticamente (GC -> finalizer -> disponer)

Sin embargo, la llamada a un método de eliminación que desuscriba de un evento nunca ocurrirá automáticamente, incluso si agrega un finalizador que llamaría a su disposición. (al menos no mientras exista el objeto propietario del evento, y si se lo llamara no se beneficiaría de la cancelación de la suscripción, ya que el objeto propietario del evento también desaparecería)

Entonces, la diferencia principal es que los eventos de alguna manera construyen un gráfico de objetos que no se puede recostackr, ya que el objeto de manejo de eventos de repente se hace referencia al servicio que solo quería referenciar / usar. De repente, se ve obligado a llamar a Dispose, ya que no es posible deshacerse automáticamente . Dispose tendría un significado más sutil que el que se encuentra en todos los ejemplos en los que una teoría de supresión de “deshace”;) – no es necesario, ya que se llamaría automáticamente (en algún momento) …

De todas formas. Dado que el patrón desechable es algo que ya es bastante complicado (se trata de finalizadores que son difíciles de corregir y de muchas pautas / contratos) y lo más importante en la mayoría de los puntos no tiene nada que ver con el tema refiriéndose al tema, diría que sería es más fácil conseguir eso separado en nuestras cabezas simplemente al no usar esa metáfora para algo que podría llamarse “unroot from object graph” / “stop” / “apague”.

Lo que queremos lograr es desactivar / detener algunos comportamientos (anulando la suscripción a un evento). Sería bueno tener una interfaz estándar como IStoppable con un método Stop (), que por contrato solo se enfoca en

  • conseguir que el objeto (+ todos sus propios elementos de detención) se desconecte de los eventos de cualquier objeto que no haya creado por sí mismo
  • para que ya no se llame en forma de estilo de evento implícito (por lo tanto, se puede percibir como detenido)
  • se puede recostackr tan pronto como desaparezcan las referencias tradicionales sobre ese objeto

Llamemos al único método de interfaz que cancela la suscripción “Stop ()”. Sabría que el objeto detenido está en un estado aceptable, pero solo se detiene. Tal vez una propiedad simple “Detenido” también sería agradable de tener.

Incluso tendría sentido tener una interfaz “IRestartable” que herede de IStoppable y, además, tiene un método “Restart ()” si solo quiere pausar un comportamiento determinado que sin duda será necesario volver a usar en el futuro, o para almacenar un eliminado modelo objeto en un historial para recuperación de deshacer posterior.

Después de todo lo escrito, tengo que confesar que acabo de ver un ejemplo de IDisposable en algún lugar de aquí: http://msdn.microsoft.com/en-us/library/dd783449%28v=VS.100%29.aspx Pero de todos modos hasta que obtener todos los detalles y la motivación original de IObservable, diría que no es el mejor ejemplo de uso

  • ya que de nuevo es un sistema bastante complicado y solo tenemos un pequeño problema aquí
  • y podría ser que una de las motivaciones de ese nuevo sistema es deshacerse de los eventos en primer lugar, lo que resultaría en una especie de desbordamiento de la stack con respecto a la pregunta original.

Pero parece que están en una buena pista. De todos modos: deberían haber utilizado mi interfaz “IStoppable”;) ya que creo firmemente que hay una diferencia en

  • Deseche: ” debe llamar a ese método o podría filtrarse algo si el GC llega tarde” …

y

  • Detener: “tienes que llamar a este método para detener cierto comportamiento

IDisposable es firmemente sobre los recursos, y la fuente de suficientes problemas para no enturbiar las aguas, creo.

También estoy votando por un método de cancelación de suscripción en su propia interfaz.

Una opción puede ser no darse de baja en absoluto, solo para cambiar lo que significa la suscripción. Si el controlador de eventos puede ser lo suficientemente inteligente como para saber lo que se supone que debe hacer en función del contexto, no es necesario cancelar la suscripción en primer lugar.

Eso puede o no ser una buena idea en su caso particular, no creo que realmente hayamos obtenido suficiente información, pero vale la pena considerarlo.

Otra opción sería usar delegates débiles o algo así como eventos débiles de WPF , en lugar de tener que darse de baja de manera explícita.

PD [OT] Considero que la decisión de proporcionar solo a los delegates fuertes el error de diseño más caro de la plataforma .NET.

No, no estás previniendo la intención de IDisposable. IDisposable está pensado como una forma de uso múltiple para garantizar que cuando haya terminado de usar un objeto, pueda limpiar proactivamente todo lo relacionado con ese objeto. No tiene que ser solo recursos no administrados, también puede incluir recursos administrados. ¡Y una suscripción de evento es solo otro recurso administrado!

Un escenario similar que se presenta con frecuencia en la práctica es que implementará IDisposable en su tipo, con el único fin de garantizar que pueda llamar a Dispose () en otro objeto administrado. Esto tampoco es una perversión, ¡es solo una ordenada gestión de recursos!