visual valid net example documentacion comment comentarios code c# weak-references

valid - summary c#



Error en WeakAction en caso de acción de cierre (3)

Desea extender el tiempo de vida de la instancia de la clase de cierre para que sea exactamente igual a la que tiene la instancia A El CLR tiene un tipo de controlador GC especial para eso: el Ephemeron , implementado como internal struct DependentHandle .

  1. Esta estructura solo se expone como parte de la clase ConditionalWeakTable . Puede crear una tabla de este tipo por WeakAction con exactamente un elemento en ella. La clave sería una instancia de A , el valor sería la instancia de clase de cierre.
  2. Alternativamente, puedes abrir Palanca DependentHandle usando la reflexión privada.
  3. O bien, podría usar una instancia de ConditionalWeakTable compartida globalmente. Probablemente requiere sincronización para su uso. Mira los documentos.

Considere abrir un problema de conexión para hacer que DependentHandle pública y vincular a esta pregunta para proporcionar un caso de uso.

En uno de los proyectos en los que participo hay un gran uso de WeakAction . Esa es una clase que permite mantener la referencia a una instancia de acción sin hacer que su objetivo no se recoja. La forma en que funciona es simple, realiza una acción en el constructor y mantiene una referencia débil al objetivo de la acción y al Método, pero descarta la referencia a la acción en sí. Cuando llega el momento de ejecutar la acción, comprueba si el objetivo sigue vivo y, si es así, invoca el método en el objetivo.

Todo funciona bien, excepto en un caso: cuando la acción se crea una instancia en un cierre. Considera el siguiente ejemplo:

public class A { WeakAction action = null; private void _register(string msg) { action = new WeakAction(() => { MessageBox.Show(msg); } } }

Dado que la expresión lambda está utilizando la variable local msg , el compilador C # genera automáticamente una clase anidada para contener todas las variables de cierre. El objetivo de la acción es una instancia de la clase anidada en lugar de la instancia de A. La acción pasada al constructor WeakAction no se hace referencia una vez que el constructor está listo y, por lo tanto, el recolector de basura puede deshacerse de él instantáneamente. Más adelante, si se ejecuta WeakAction , no funcionará porque el objetivo ya no está vivo, aunque la instancia original de A esté viva.

Ahora no puedo cambiar la forma en que se llama WeakAction (ya que se usa ampliamente), pero puedo cambiar su implementación. Estaba pensando en tratar de encontrar una manera de obtener acceso a la instancia de A y forzar a la instancia de la clase anidada a permanecer con vida mientras la instancia de A aún está viva, pero no sé cómo hacerlo.

Hay muchas preguntas sobre lo que A tiene que ver con cualquier cosa, y sugerencias para cambiar la forma en que A crea una acción débil (lo que no podemos hacer), así que aquí hay una aclaración:

Una instancia de la clase A desea que una instancia de la clase B notifique cuando ocurre algo, por lo que proporciona una devolución de llamada utilizando un objeto Action . A no es consciente de que B usa acciones débiles, simplemente proporciona una Action para servir como devolución de llamada. El hecho de que B use WeakAction es un detalle de implementación que no está expuesto. B necesita almacenar esta acción y utilizarla cuando sea necesario. Pero B puede vivir mucho más tiempo que A , y mantener una fuerte referencia a una Acción normal (que por sí misma contiene una referencia fuerte de la instancia de A que la generó) hace que A nunca se recoja basura. Si A es parte de una lista de elementos que ya no están vivos, esperamos que A sea ​​recogida de basura, y debido a la referencia que B tiene de la Acción, que por sí misma apunta a A , tenemos una pérdida de memoria.

Así que en lugar de que B contenga una Acción que A proporcionó, B envuelve en una WeakAction y almacena solo la acción débil. Cuando llega el momento de llamarlo, B solo lo hace si la WeakAction todavía está viva, lo que debería ser siempre y cuando A esté vivo.

A crea esa acción dentro de un método y no mantiene una referencia a él por su cuenta, eso es un hecho. Dado que la Action se construyó en el contexto de una instancia específica de A , esa instancia es el objetivo de A , y cuando A muere, todas las referencias débiles se vuelven null por lo que B sabe que no debe llamarlo y se deshace del objeto WeakAction .

Pero a veces el método que generó la Action usa variables definidas localmente en esa función. En cuyo caso, el contexto en el que se ejecuta la acción incluye no solo la instancia de A , sino también el estado de las variables locales dentro del método (que se denomina "cierre"). El compilador de C # hace eso creando una clase anidada oculta para contener estas variables (llamémoslo A__closure ) y la instancia que se convierte en el objetivo de la Action es una instancia de A__closure , no de A Esto es algo que el usuario no debe tener en cuenta. Excepto que esta instancia de A__closure solo es referenciada por el objeto Action . Y dado que creamos una referencia débil al objetivo y no mantenemos una referencia a la acción, no hay ninguna referencia a la instancia de A__closure , y el recolector de basura puede (y generalmente lo hace) deshacerse de ella instantáneamente. Así que A vive, A__closure muere y, a pesar del hecho de que A todavía espera que se invoque la devolución de llamada, B no puede hacerlo.

Ese es el error.

Mi pregunta fue si alguien sabe de alguna manera que el constructor de WeakAction , la única pieza de código que realmente contiene el objeto original de Action, temporalmente, puede extraer de alguna manera mágica la instancia original de A de la instancia de A__closure que encuentra en el Target de la Action . Si es así, tal vez podría extender el ciclo de vida del A__Closure para que coincida con el de A


Después de investigar un poco más y de recopilar toda la información útil de las respuestas que se publicaron aquí, me di cuenta de que no habrá una solución elegante y sellada al problema. Como este es un problema de la vida real, optamos por un enfoque pragmático, intentando al menos reducirlo manejando la mayor cantidad de escenarios posible, así que quería publicar lo que hicimos.

Una investigación más profunda del objeto Action que se pasa al constructor de WeakEvent, y especialmente a la propiedad Action.Target, mostró que efectivamente hay 2 casos diferentes de objetos de cierre.

El primer caso es cuando Lambda usa variables locales del alcance de la función de llamada, pero no usa ninguna información de la instancia de la clase A. En el siguiente ejemplo, suponga que EventAggregator.Register es un método que realiza una acción y almacena una WeakAction que lo envuelve.

public class A { public void Listen(int num) { EventAggregator.Register<SomeEvent>(_createListenAction(num)); } public Action _createListenAction(int num) { return new Action(() => { if (num > 10) MessageBox.Show("This is a large number"); }); } }

La lambda creada aquí utiliza la variable num , que es una variable local definida en el alcance de la función _createListenAction . Así que el compilador tiene que envolverlo con una clase de cierre para mantener las variables de cierre. Sin embargo, dado que la lambda no accede a ninguno de los miembros de clase A, no es necesario almacenar una referencia a A. Por lo tanto, el objetivo de la acción no incluirá ninguna referencia a la instancia A y no hay ninguna posibilidad para el constructor WeakAction para alcanzarlo.

El segundo caso se ilustra en el siguiente ejemplo:

public class A { int _num = 10; public void Listen() { EventAggregator.Register<SomeEvent>(_createListenAction()); } public Action _createListenAction() { return new Action(() => { if (_num > 10) MessageBox.Show("This is a large number"); }); } }

Ahora _num no se proporciona como parámetro para la función, proviene de la instancia de clase A. El uso de la reflexión para conocer la estructura del objeto Target revela que el último campo que define el compilador contiene una referencia a la instancia de clase A. Este caso también se aplica cuando la lambda contiene llamadas a métodos de miembros, como en el siguiente ejemplo:

public class A { private void _privateMethod() { // do something here } public void Listen() { EventAggregator.Register<SomeEvent>(_createListenAction()); } public Action _createListenAction() { return new Action(() => { _privateMethod(); }); } }

_privateMethod es una función miembro, por lo que se llama en el contexto de la instancia de clase A, por lo que el cierre debe mantener una referencia a ella para invocar a la lambda en el contexto correcto.

Entonces, el primer caso es un Cierre que solo contiene funciones de la variable local, el segundo contiene una referencia a la instancia A principal. En ambos casos, no hay referencias concretas a la instancia de Closure, por lo que si el constructor WeakAction simplemente deja las cosas como están, WeakAction "morirá" instantáneamente a pesar del hecho de que la instancia de clase A todavía está viva.

Nos enfrentamos aquí con 3 problemas diferentes:

  1. ¿Cómo identificar que el objetivo de la acción es una instancia de clase de cierre anidada, y no la instancia A original?
  2. ¿Cómo obtener una referencia a la instancia original de clase A?
  3. ¿Cómo extender la vida útil de la instancia de cierre para que viva mientras viva la instancia de A, pero no más allá de eso?

La respuesta a la primera pregunta es que confiamos en 3 características de la instancia de cierre: - Es privada (para ser más precisos, no es "Visible". Cuando se usa el compilador C #, el tipo reflejado tiene el valor IsPrivate establecido en verdadero pero con VB no lo hace. En todos los casos, la propiedad IsVisible es falsa). - Está anidado. - Como @DarkFalcon mencionó en su respuesta, está decorado con el atributo [CompilerGenerated].

private static bool _isClosure(Action a) { var typ = a.Target.GetType(); var isInvisible = !typ.IsVisible; var isCompilerGenerated = Attribute.IsDefined(typ, typeof(CompilerGeneratedAttribute)); var isNested = typ.IsNested && typ.MemberType == MemberTypes.NestedType; return isNested && isCompilerGenerated && isInvisible; }

Si bien este no es un predicado sellado al 100% (un programador malintencionado puede generar una clase privada anidada y decorarlo con el atributo Generador del compilador), en escenarios de la vida real esto es lo suficientemente preciso, y nuevamente, estamos construyendo una solución pragmática, no académica. uno.

Entonces el problema número 1 está resuelto. El constructor de acciones débiles identifica situaciones donde el objetivo de la acción es un cierre y responde a eso.

El problema 3 también es fácilmente solucionable. Como @usr escribió en su respuesta, una vez que obtenemos una retención de la instancia de la clase A, agregando una ConditionalWeakTable con una sola entrada donde la instancia de la clase A es la clave y la instancia de cierre es el objetivo, resuelve el problema. El recolector de basura sabe que no debe recopilar la instancia de cierre mientras viva la instancia de clase A. Así que eso está bien.

El único problema que no se puede resolver es el segundo, ¿cómo obtener una referencia a la instancia de clase A? Como dije, hay 2 casos de cierres. Uno donde el compilador crea un miembro que contiene esta instancia, y otro donde no lo hace. En el segundo caso, simplemente no hay forma de obtenerlo, por lo que lo único que podemos hacer es crear una referencia firme a la instancia de cierre para evitar que se recoja basura de forma instantánea. Esto significa que puede vivir la instancia de clase A (de hecho, vivirá mientras viva la instancia de WeakAction, que puede ser para siempre). Pero este no es un caso tan terrible después de todo. La clase de cierre en este caso solo contiene algunas variables locales, y en el 99.9% de los casos es una estructura muy pequeña. Si bien esto sigue siendo una pérdida de memoria, no es sustancial.

Pero solo para permitir que los usuarios eviten incluso la pérdida de memoria, ahora hemos agregado un constructor adicional a la clase WeakAction, de la siguiente manera:

public WeakAction(object target, Action action) {...}

Y cuando se llama a este constructor, agregamos una entrada ConditionalWeakTable donde el objetivo es la clave y el objetivo de las acciones es el valor. También tenemos una referencia débil tanto al objetivo como a las acciones, y si alguno de ellos muere, eliminamos ambos. Para que el objetivo de las acciones no viva menos ni más que el objetivo proporcionado. Básicamente, esto le permite al usuario de WeakAction decirle que se aferre a la instancia de cierre mientras el objetivo viva. Por lo tanto, se les pedirá a los nuevos usuarios que lo usen para evitar pérdidas de memoria. Pero en los proyectos existentes, donde no se usa este nuevo constructor, al menos esto minimiza las fugas de memoria a cierres que no tienen referencia a la instancia de clase A.

El caso de cierres que hacen referencia al padre es más problemático porque afecta a la colección garbase. Si mantenemos una referencia firme al cierre, causamos una pérdida de memoria mucho más drástica porque la instancia de clase A tampoco se borrará nunca. Pero este caso también es más fácil de tratar. Como el compilador agrega un último miembro que contiene una referencia a la instancia de clase A, simplemente usamos la reflexión para extraerlo y hacemos exactamente lo que hacemos cuando el usuario lo proporciona en el constructor. Identificamos este caso cuando el último miembro de la instancia de cierre es del mismo tipo que el tipo declarante de la clase anidada de cierre. (Nuevamente, no es 100% preciso, pero para los casos de la vida real es lo suficientemente cerca).

Para resumir, la solución que presenté aquí no es una solución 100% sellada, simplemente porque no parece haber tal solución. Pero como tenemos que proporcionar ALGUNAS respuestas a este error molesto, esta solución al menos reduce sustancialmente el problema.


a.Target proporciona acceso al objeto que contiene los parámetros lambda. Realizar un GetType en esto devolverá el tipo generado por el compilador. Una opción sería verificar este tipo para el atributo personalizado System.Runtime.CompilerServices.CompilerGeneratedAttribute y mantener una referencia segura al objeto en este caso.

Now I can''t change the way the WeakAction is called, (since it''s in wide use) but I can change it''s implementation. Tenga en cuenta que esta es la única forma hasta ahora que puede mantenerlo vivo sin necesidad de modificar la WeakAction se construye WeakAction . Tampoco alcanza el objetivo de mantener viva la lambda mientras el objeto A (en su lugar, la mantendría viva tanto como la WeakAction ). No creo que se pueda lograr sin cambiar la WeakAction se construye WeakAction como se hace en las otras respuestas. Como mínimo, WeakAction necesita obtener una referencia al objeto A , que actualmente no proporciona.