Ciclos de Referencia en Closures

Tutorial Swift – Ciclos de Referencia en Closures

Josué V. Herrera Swift Avanzado, Tutoriales 0 Comments

En este Tutorial Swift aprenderemos sobre los ciclos de referencia en closures, conjugando así dos temas bien apasionantes como son la gestión de memoria y los closures.

Seguro recordarán que en un artículo anterior (no muy lejano en el tiempo) sobre el manejo de la memoria y ARC en el lenguaje Swift, hablamos acerca de los distintos tipos de enlaces que adoptan las clases entre sí. Al mismo tiempo ya conocemos la respuesta a las preguntas ¿Qué es un Closure? y ¿Cuándo usar un Closure?, pues en estos tutoriales también aprendimos que un closure es un tipo por referencia. Al escuchar esto último nos pudiera surgir la siguiente pregunta:

¿Si un closure es un tipo por referencia, pudiera entonces generar un ciclo de retención?

La respuesta es sí, pueden crear un ciclo de retención o lo que es lo mismo un ciclo de referencia fuerte al igual que las clases.

Para comenzar a interactuar con este evento haremos uso del ejemplo utilizado en el artículo anteriormente mencionado sobre el manejo de la memoria. El presente artículo aborda temas avanzados y por ende asumo que todos sean capaces de leer código e interpretarlo, si tienen alguna duda, en los comentarios podeis dejarla.

Para comenzar a hacer uso del código de este ejemplo he creado un nuevo branch asociado a este artículo, así que solamente tendremos que ejecutar el siguiente comando en la raíz de nuestra carpeta de proyecto:

…la salida de este comando sería:

Crear Repositorio Git - Single Branch

Luego de esto abrimos nuestro proyecto y vamos a la clase de nombre Accountant la cual se encarga de llevar un registro del patrimonio de una persona que en el caso específico del ejemplo está representado con la clase Person.

En esta clase definimos un typealias, NetWorthChanged, el cual es un closure que toma como parámetro un tipo Double (como el valor del patrimonio) y no retorna nada. Tenemos también dos propiedades: netWorthChangedHandler que es un closure opcional al cual llamamos cuando el valor del patrimonio cambia y netWorth donde almacenamos el valor total del patrimonio de una persona. Esta última propiedad cuenta con un observador didSet que ejecuta el closure netWorthChangedHandler si este no es nil. Finalmente la función gainedNewAsset debería de ser llamada para informar a la instancia que el valor de un nuevo bien debe ser añadido al patrimonio.

Continuaremos por modificar la clase Person en pos de comenzar a hacer uso de la nueva clase Accountant. Luego de esta modificación el archivo Person.swift debe lucir como a continuación muestro:

A la clase Person hemos añadido la propiedad de nombre accountant en la línea 6 y cuyo valor por defecto es una nueva instancia de tipo Accountant. Desde este punto ya detectamos un enlace fuerte desde una futura instancia de Person hacia su instancia local de Accountant. Dentro del inicializador init() hemos igualado a netWorthChangedHandler con un closure donde efectuamos una llamada a la función local netWorthDidChange que registra el nuevo valor del patrimonio. Por último modificamos la función takeOwnershipOfAsset para que notifique a la instancia de Accountant acerca de las nuevas propiedades adquiridas.

Antes de continuar muestro el contenido del fichero main.swift por aquellos que quizás no han bajado el código de github:

Si compilamos y corremos nuestra aplicación de consola la salida debe lucir como la siguiente:

Fuga de Memoria

En la salida en pantalla nos percatamos de que tenemos una fuga de memoria (memory leak), las instancias laptop y hat no han sido liberadas, tampoco la instancia de CreditCard llamada masterCard y evidentemente tampoco la instancia de Person de nombre gochi. Pero ¿qué ha pasado? antes de las nuevas modificaciones el código funcionaba perfectamente y sin embargo ahorita nos encontramos con el mismo problema.

La razón está asociada a una referencia fuerte que hemos añadido y que puede no ser muy obvia. Seguramente lo que nos viene rápidamente a la mente es que la clase Person tiene una referencia fuerte hacia Accountant, pero esta última no cuenta con ninguna hacia Person, entonces aparentemente no debería de haber ningún problema.

Cuando analizamos un poco más a fondo y recordamos la capacidad que tiene los closure de capturar las variables de su entorno comenzamos a sospechar del inicializador, específicamente de la línea 16:

self.netWorthDidChange(netWorth: $0)

…que tal y como podemos observar hacemos uso de self.

Como parte de nuestra investigación proseguiremos a eliminar self, lo haremos en un intento de que cuando el closure se ejecute en la instancia de Accountant no haya una referencia explícita hacia Person. La nueva línea quedaría entonces como:

netWorthDidChange(netWorth: $0)

…y volvemos a compilar. Al hacerlo nos salta un error en tiempo de compilación donde se nos informa los siguiente:

Call to method ‘netWorthDidChange’ in closure requires explicit ‘self.’ to make capture semantics explicit

En este mensaje de error podemos leer / interpretar que para efectuar una llamada a netWorthDidChange dentro del closure tenemos que hacerlo mediante self ya que la captura de valores tiene que ser semánticamente explícita. Con respecto a esto último, según he leído, Swift nos obliga a usar self para forzarnos a considerar que un ciclo de referencia fuerte es posible.

Recordemos que un closure cuenta con un ámbito propio dentro de su definición y que por defecto genera referencias fuertes hacia cualquiera de las variables que este usa dentro de su ámbito. Así que nuestra sospecha es correcta, cuando llamamos a netWorthDidChange mediante self estamos creando una referencia fuerte desde Accountant hacia Person, mientras que ya teníamos una desde Person hacia Accountant,  es decir que hemos generado mediante un closure un ciclo de retención con todas las de la ley.

En el diagrama que muestro a continuación podemos constatar lo antes explicado:

Ciclos de Referencia en Closures

Lista de Captura

La solución a este problema reside en una lista de captura de closures (closure capture list en inglés). Modifiquemos el inicializador de Person para que luzca así:

En la línea 7 es donde establecemos la lista de captura, en esta establecemos como weak la referencia que genera self. Como su propio nombre indica, una lista de captura es una lista, así que nos permite declarar más de una referencia siguiendo la sintaxis:

Es decir, separaríamos las referencias por comas siempre antecediéndolas con el modificador respectivo. En el caso de un closure con un formato simplificado (como el ejemplo de Person.init) siempre tenemos que añadir in tras la lista de captura.

Pero todo no termina aquí. Si analizamos bien el gráfico y la solución propuesta en la nueva versión de Person.init, quizás podamos aplicar una última mejora. Comencemos por tener en cuenta que Person tiene una referencia fuerte hacia Accountant mediante una instancia local de nombre accountant, así que el enlace que se genera desde Accountant hacia Person mediante el closure jamás será nil y esto es debido a que el closure se crea sobre esa misma instancia local (accountant), cuya existencia en memoria depende de Person. Dicho esto y siguiendo las pautas determinadas en la documentación oficial sobre los ciclos de retención en closures y específicamente sobre la lista de captura:

If the captured reference will never become nil, it should always be captured as an unowned reference, rather than a weak reference.

Traduciendo esta nota en la documentación: siempre y cuando tengamos la certeza de que una referencia jamás será nil, el enlace debe ser siempre capturado como unowned en la lista de captura.

Así que modificamos nuevamente Person.init hacia esta versión definitiva:

En este punto salvamos y compilamos nuevamente, la salida en pantalla debe lucir como:

Como podemos observar ya el código funciona correctamente.

Me gustaría comentar, antes de finalizar, que la razón de añadir la lista de captura en la línea 7 es meramente por legibilidad y limpieza en el código.

Pero he visto como algunos programadores la establecen luego de abrir las llaves del closure:

Como he comentado en otras ocasiones la elección recae en ustedes, yo personalmente pienso que esta última variante hace que la primera línea luzca ligeramente más críptica y quizás tengamos que hacer una pausa, mientras que la primera opción luce más clara y se puede leer más rápido sin lugar a equivocaciones.

El código de ejemplo lo pueden encontrar en un proyecto de consola alojado en la cuenta personal de @josuevhn en GitHub, específicamente en el repositorio asociado a este artículo. Este código cuenta con dos branches, uno asociado al artículo donde aprendimos sobre la gestión de memoria, mientras que el otro branche recoge las modificaciones asociadas al artículo actual. Dicho esto recomiendo que se use el comando que mostré al inicio de este tutorial en pos de evitar equivocaciones entre los dos branches.

Espero que todo cuanto se ha dicho aquí, de una forma u otra le haya servido de aprendizaje, de referencia, que haya valido su preciado tiempo.

Este artículo, al igual que el resto, será revisado con cierta frecuencia en pos de mantener un contenido de calidad y actualizado.

Cualquier sugerencia, ya sea errores a corregir, información o ejemplos a añadir será, más que bienvenida, necesaria!