No es usual, pero a veces… pasa. Los Objetos que mantienen referencias cruzadas entre sí son un buen sitio en los que buscar cuando se trata de encontrar las fugas de memoria en tu aplicación. Esto es, cuando una “ClassA” necesita conocer sobre una “ClassB” (es decir, mantener una referencia sobre una instancia), y la “ClassB” necesita conocer sobre “ClassA”. Continúa leyendo para saber más de lo que estoy hablando… ¡y de como solucionar el problema!
Si estás lidiando con una situación en el código de tu app donde dos clases han de mantener una referencia sobre la otra, entonces probablemente esté creando una situación de fuga de memoria en el caso de que no hayas tenido cuidado.
Veámoslo mediante un simple ejemplo para ver con mayor claridad de lo que estoy hablando.
- Crea un nuevo proyecto de Xojo y añade dos nuevas clases llamadas ClassA y ClassB.
- Añade una propiedad classBReference As ClassB en ClassA.
- Añade una propiedad classAReference As ClassA en ClassB.
- Añade un Constructor a ClassB utilizando la siguiente signatura:
Constructor( reference As ClassA)
- Y añade esta línea de código a dicho método:
me.classAReference = reference
- A continuación, añade el método Destructor a ambas clases y escribe “Break” como la única línea de código para dicho método en cada clase. Esto nos permitirá ver si se está produciendo una fuga de memoria… o no. Por ejemplo, si cuando ejecutes el programa de ejemplo el Depurador no se detiene en las líneas Break, eso significará que no se está ejecutando el Destructor de las instancias… y, por tanto, los objetos no han sido liberados de la memoria. ¡Habrá una fuga de memoria!
- Añade ahora el evento Opening a Window1 y escribe estas dos líneas de código en el Editor de Código asociado:
CreateReferences Break
Como puedes ver, está llamando al método CreateReferences… y luego se detiene en el depurador.
- Añade ahora el método CreateReferences en Window1 y escribe estas líneas de código en el Editor de Código asociado:
Var ca As New ClassA Var cb As New ClassB(ca) ca.ClassBReference = cb
Es fácil ver lo que está pasando aquí: La instancia creada a partir de la clase ClassB está manteniendo una referencia “dura” sobre ClassA (mediante la instancia ca), y luego la instancia ca mantiene una referencia “dura” sobre la instancia cb creada a partir de la clase ClassB.
Ejecuta el proyecto. ¿Se está deteniendo el depurador en las líneas Break correspondientes a los métodos Destructor de ClassA y ClassB? No; ¡se está produciendo una fuga de memoria porque ambas mantienen referencias duras entre sí!
Puedes verlo más fácilmente accediendo en el Panel Depurador a Global > Runtime > Contents. ¡Ambos objetos aún están “vivos” en memoria!
¡WeakRef al rescate!
Pero digamos que el diseño de tu app necesita mantener dichas referencias; así que… ¿cómo podemos resolver esta situación? La respuesta se encuentra en el uso de referencias débiles en vez de emplear referencias duras, y el lenguaje Xojo tiene una clase para ello: WeakRef.
Vamos a cambiar las propiedades de ClassA y ClassB de modo que su tipo sea ahora WeakRef; de modo que sus nuevas definiciones serán:
ClassBReference As WeakRef ClassAReference As WeakRef
Y cambiemos también el código del Constructor correspondiente a ClassB para que cree una nueva instancia WeakRef a partir del objeto recibido, en vez de asignarlo directamente a la propiedad:
ClassAReference = new WeakRef(reference)
Cambiemos también el código en el método CreateReferences de modo que ahora sea:
Var ca As New ClassA Var cb As New Classb(ca) ca.ClassBReference = new WeakRef(cb)
Ejecuta de nuevo el proyecto de ejemplo. Ahora verás que el Depurador se detiene en las líneas “Break” correspondientes a los métodos Destructor… lo que significa que ambos objetos quedan correctamente liberados de la memoria. ¡Problema resuelto!
Consideraciones sobre WeakRef
Cuando se utilizan objetos WeakRef a lo largo y ancho del código, has de tener en cuenta que para acceder al objeto real que están referenciando has de utilizar la propiedad WeakRef.value… y muy probablemente debas de hacer un Cast del valor obtenido sobre la clase original.
Para verlo con mayor claridad, añade el método “SayHello” a la clase ClassB y escribe esta línea de código en el Editor de Código asociado:
MessageBox("Hi From ClassB Instance")
A continuación, selecciona el método CreateReferences de Window1 y añade la siguiente línea de código al final:
ClassB(ca.ClassBReference.Value).SayHello
En primer lugar estamos accediendo a la propiedad ClassBReference correspondiente a la instancia ca, lo cual nos da acceso a la instancia WeakRef de la propiedad; de modo que hemos de acceder a su propiedad Value para obtener la instancia de objeto en la que estamos interesados realmente: la instancia de ClassB.
Pero, como la propiedad Value de WeakRef devuelve un objeto genérico (tipo de dato Object), necesitamos hacer un Cast del mismo sobre la clase con la que realmente deseamos trabajar (y que deberíamos de saber cual es). En este caso se trata de una instancia creada a partir de la clase “ClassB”, de modo que es por ello por lo que hacemos un Cast sobre dicha clase usando la expresión:
ClassB(ca.ClassBReference.Value)
Luego, y gracias a que hemos hecho un Cast a la clase ClassB, podemos continuar utilizando la notación por punto para acceder a cualquiera de los métodos / funciones / propiedades expuestos por la clase ClassB; en este caso, el método “SayHello”.
Ejecuta de nuevo la aplicación de ejemplo, y ahora deberías de ver que, de hecho, la instancia de ClassB referenciada por la instancia de la clase ClassA está mostrando un MessageBox.
Además, cuando accedes a las propiedades de una clase que mantienen referencias duras sobre objetos, probablemente harás (o deberías de hacer) comprobaciones del tipo:
If ca.ClassBReference <> Nil Then ca.ClassBReference.SayHello End If
Pero cuando se utilizan WeakRefs deberás de hacer una doble comprobación: primero contra la instancia WeakRef propiamente dicha, y luego sobre la instancia devuelta mediante la propiedad Value. De modo que la anterior comprobación se transformaría en:
If ca.ClassBReference <> Nil And ca.ClassBReference.Value <> Nil then ClassB(ClassBReference.Value).SayHello End If
Aclarado final
Con todo lo anterior, mantener objetos con referencias cruzadas duras entre sí probablemente no sea el mejor de los diseños… pero a veces es necesario. En el caso de que el diseño de tu aplicación deba de seguir ese camino… simplemente asegúrate de utilizar referencias débiles mediante la clase WeakRef en vez de emplear referencias duras.
Mantenlo en tu memoria… ¡y feliz programación con Xojo!