En el Tutorial Swift de hoy tocaremos un tema que ya no podemos seguir alargando, los tipos por referencia y por valor. El manejo de la memoria es un tema que personalmente me encanta y nos acerca como ningún otro a lo que a su vez conforma uno de los pilares de la programación.

Tipos por referencia vs tipos por valor

Antes de comenzar formulemos la siguiente pregunta ¿cuáles son las principales diferencias entre estos dos tipos de datos? La respuesta rápida sería que los tipos por valor son aquellos que mantienen una copia única de sus datos siendo independientes de otras variables similares, mientras que los tipos por referencia apuntan a la posición de memoria donde se encuentra la instancia de ciertos datos, estos también pueden compartir una misma versión de los datos con otras referencias.

Imaginemos varios cajones, cada cajón sería el homólogo a una dirección de memoria y el contenido del mismo pues el valor almacenado en esa dirección de memoria. Un tipo por referencia sería análogo a tener una nota en nuestro escritorio o haber escrito un correo al trabajador nuevo informando sobre cual cajón es el de las herramientas, mientras que un tipo por valor sería análogo a un solo papel pero esta vez pegado en el cajón en el cual se puede leer “Herramientas”. Es entendible que existan varias referencias al cajón de las herramientas, pero no tendría sentido y sí extremadamente redundante que en el propio cajón de las herramientas tengamos varios papeles pegados que informen sobre lo mismo.

A continuación una lista de los tipos de datos por referencia y por valor:

Tipos por Referencia

  • class

Tipos por Valor

  • struct
  • enum
  • tuple
  • Todos los tipos incorporados (built-in types) por el lenguaje (Int, Double, Float, Bool, String, Array, Dictionary).

Tipos por referencia

Las clases en Swift son el único tipo por referencia con el que contamos, veamos sus características, analicemos el siguiente ejemplo:

…al inicio declaramos la clase Dog (en representación a un perro) como parte de esta también hemos añadido una propiedad booleana llamada wasFed que contendrá el estado de si nuestro perrito digital fue alimentado o no. En la línea 11 creamos una referencia constante llamada dog a una instancia de nuestra clase, instancia que es almacenada en una posición de memoria localizada en el HEAP, en la siguiente línea creamos otra constante esta vez llamada puppy y la igualamos a la constante dog con lo cual la volvemos en otra referencia a la misma posición de memoria que ocupa nuestra instancia y a la cual dog apunta, y esta como ya comentamos es la característica principal de los tipos por referencia, cuando dos variables o constantes por referencia son igualadas ambas terminan apuntando a la misma versión de los datos:

Swift Tipos por Referencia

…como podemos ver en esta imagen ambas constantes apuntan a la misma posición de memoria. En la línea 15 establecemos a true el valor de la propiedad wasFed de la constante puppy.

Pero, ¿Cómo podemos asignar un valor a esta propiedad si puppy es constante? Pues cuando declaramos puppy como constante lo que estamos estableciendo es que puppy como referencia no podrá apuntar a otra instancia que a la inicial, es decir que no podrá hacer referencia a otra posición de memoria que la que ocupa la instancia inicial creada en la línea 11, por este motivo y por el hecho de que la propiedad wasFed es variables es que podemos modificar su valor.

…continuemos con nuestro ejemplo luego de haber alimentado a nuestro cachorrillo, terminamos por imprimir los valores de las propiedades wasFed de las referencias dog y puppy, que luego de lo antes explicado ya sabemos que son iguales, en este caso true.

Ahora si queremos realmente constatar lo que les acabo de comentar ejecutemos esta modificación del código anterior:

…las modificaciones añadidas imprimen la dirección de memoria a la que apuntan las dos referencias usadas, la salida en pantalla sería:

…ojo que las direcciones de memoria pueden variar en el tiempo y de un ordenador a otro, dicho esto es muy improbable que obtengan la misma dirección de memoria que yo.

Operadores de identidad

Algunas veces dadas ciertas situaciones quizás necesitemos conocer si dos referencias están apuntando a la misma dirección de memoria, es decir a la misma versión de los datos. Esto lo podemos lograr haciendo uso de los operadores de identidad:

  • Idéntico a (===)
  • No idéntico a (!==)

…veamos un ejemplo de su uso:

…la salida en pantalla del código anterior es:

…en la línea 19 creamos una instancia de la clase Dog llamada seniorDog, en este punto ya es sabido que las direcciones de memoria serán distintas ya que estamos creando una nueva instancia, por este motivo es que el bloque if de las líneas 27 a la 31 es el que se ejecuta imprimiendo el mensaje correspondiente como podemos comprobar en la salida en pantalla, en las últimas líneas de esta podemos ver que efectivamente las direcciones de memoria son distintas.

Tipos por valor

Los tipos por valor funcionan completamente diferente, exploremos sus características a través de un simple ejemplo:

…el código habla por sí mismo, y ahora teniendo en cuenta lo antes comentado sobre los tipos por referencia y luego de aumentar el valor de b en una unidad ¿cual sería el resultado impreso por las últimas línea? pues:

…como podemos constatar cada variable cuenta con una copia independiente de los datos, es decir que cada una apunta a una dirección de memoria distinta. Lo mismo aplica para el resto de tipos por valor, veamos otro ejemplo:

…la salida en pantalla:

…aquí sucede lo mismo, aún cuando igualamos kitty con el contenido de cat, kitty al ser un tipo por valor no apunta a la posición de memoria de cat en lugar de esto cuenta con una copia totalmente aparte de los datos, por lo que cuando establecemos wasFed a true no afectamos a cat y por eso obtenemos la salida antes mostrada.

Swift Tipos por Valor

En esta imagen tenemos una representación gráfica de lo antes explicado, en el ejemplo de los tipos por referencia teníamos un solo dato en memoria y en este último tenemos dos, aún asumiendo que ambos datos son del mismo tamaño es evidente que el consumo de recursos es mayor ya que en lugar de apuntar a una dirección de memoria común en el caso de los tipos por valor tenemos que pasar por el proceso de asignación e inicialización y esto tiene un costo, ¿no? ya veremos si el costo es realmente grande.

¿Cuál tipo elegir?

Hay muchas ocaciones donde el trabajo con ciertas librerías nos obligan a usar un tipo u otro, incluso hasta ambos, en mi opinión la decisión siempre dependerá de las necesidades de lo que estemos desarrollando, de cuan conveniente sea usar un tipo u otro para con nuestras intenciones. Como podemos constatar al inicio de este artículo, hay cierta tendencia por parte de Swift a usar tipos por valor en lugar de tipos por referencia y aunque no he leído sobre la razón oficial imagino que esto se debe a que cuando trabajamos con tipos por referencia se impone un poco más de cuidado sobre todo en proyectos grandes y en situaciones de múltiples hilos donde podemos tener varias referencias apuntando a un segmento de memoria común, arriesgándonos a usar una versión de estos datos que no es la deseada, eventos que de producirse nos lanzan a un proceso de debug bastante engorroso.

En el caso de los tipos por valor su propia naturaleza “simple” nos facilita un poco el trabajo, ya que podemos controlar sin mucho esfuerzo la mutación de los datos y las versiones de los mismos, y no podemos olvidar todo lo que hemos visto aquí, Swift ha hecho muy buen trabajo con los tipos por valor, ha reducido al máximo el consumo asociado a estos tipos de datos, sobre todo las operaciones de asignación e inicialización de memoria.

Teniendo en cuenta lo que acabamos de decir veamos a continuación como ciertos problemas de diseños nos van guiando por el buen camino, analicemos el siguiente ejemplo:

…en el código anterior hemos creado una estructura llamada Address, una clase llamada Person y otra estructura llamada Bill. La estructura Address engloba algunos pocos datos relacionados con una dirección, información que puede manejar perfectamente una estructura, un tipo por valor, asumiremos de que cada persona vive en una dirección distinta, y aún habiendo familiares, las personas se mudan con cierta frecuencia, se rentan hoy aquí y mañana allá, no tendría mucho sentido que la dirección de memoria de Address fuera compartida y tener que crear todo un mecanismo para referenciar la propiedad address de la clase Person a otro segmento de memoria cuando siendo esta una estructura no tenemos que hacer nada más, cada dirección es única, incluso estando repetida la dirección el costo como ya vimos es prácticamente ínfimo. La clase Person representa los datos que necesitamos de cada persona, y es entendible de que su dirección de memoria sea compartida entre varias áreas del código con un objeto de tipo Person, bien básica y sencilla su implementación, no hay mucho que decir al respecto. En el caso de Bill aunque es una estructura y siempre habrá una copia única (lógicamente) de cada factura, una de sus propiedades es de tipo por referencia y esto no es lo que queremos ¿Por qué? pues por lo que sucede en el siguiente ejemplo:

…en la línea 1 creamos una dirección, en la 3 una persona y en la 5 una factura, hasta aquí todo bien. Ahora, en la línea 7 hemos creado una factura nueva a partir de la factura que ya teníamos y teniendo en cuenta que Bill es una estructura lo que ha sucedido es que hemos creado una nueva versión de los datos de bill_1.

Nota: Próximamente en este sitio hablaremos sobre una característica del lenguaje Swift y en especial de los tipos por valor llamada Copy On Write, la cual consiste (básicamente) en la no duplicidad de los datos antes un evento de copia siempre y cuando estos sean dos tipos por valor y sus valores no se hayan modificado, es decir al inicio. Para más información sobre este interesante tópico seguir en enlace brindado en este párrafo.

En la línea 9 cambiamos el nombre de nuestra persona y aquí es donde se genera el problema, las líneas 11 y 12 lo demuestran, la salida en pantalla es la siguiente:

…aquí podemos comprobar como el cambio del nombre en la línea 9 corrompió a bill_2 y esto no es lo deseado. En este ejemplo todo parece muy evidente y muy fácil de corregir, ahora imaginen que lo mostrado en él esté disuelto entre cientos de líneas y varios ficheros de código y verán como todo se torna un poco más oscuro y engorroso.

Copiando referencias durante la inicialización

Veamos una de las varias maneras de corregir el comportamiento del código anterior, para comenzar modifiquemos la implementación de Bill a la siguiente versión:

…los cambios introducidos se encuentran de la línea 6 a la 11 donde hemos declarado de manera explícita el constructor de nuestra estructura, luego en la línea 9 creamos un objeto Person a partir del parámetro de entrada y con esto aislamos la referencia que contiene los argumentos creando una nueva a partir de estos, lo cual nos deja con que cada factura contiene una copia única de Person por lo que la modificación que hacemos del nombre en la línea 23 no va a afectar a nuestra factura. Pero esto no es todo, seguimos teniendo un problema y es que aun podemos acceder a billedTo y modificar su contenido.

Usando propiedades computadas

Pensando en resolver esto último lo lógico sería establecer la propiedad billedTo a privada, y crear propiedades computadas en pos de obtener y modificar sus valores, esto nos daría más control sobre como se llevan a cabo dichas operaciones. Previo a la implementación de lo antes comentado el lector tiene que estar familiarizado con los modificadores de acceso en el lenguaje de programación Swift, tema sobre el que ya se habló en otro artículo.

No obstante digamos que los niveles de acceso están implementados de la siguiente manera:

  • private: Accesible solamente desde el propio archivo de código fuente.
  • internal: Accesible solamente desde el propio módulo o proyecto.
  • public: Accesible desde cualquier lugar.

Hacemos este paréntesis ya que como estamos usando Playground, el cual solamente cuenta con un solo archivo, cuando establezcamos nuestra propiedad como privada esta aún seguirá siendo accedida por nosotros, mientras que en un entorno real esto no sería así.

Comentado esto sigamos con la nueva versión de nuestro ejemplo:

En la línea 5 establecimos nuestra variable billedTo como privada por lo que ahora solamente podemos acceder a esta a través de las propiedades computadas billedToForRead y billedToForWrite. La primera retorna a billedTo y en la segunda nos apoyamos en el bloque get en el cual como primera acción creamos una nueva instancia de Person y la inicializamos con los mismos datos de la referencia actual, es decir que hemos creado un nuevo segmento de memoria para que pueda almacenar los cambios que vamos a efectuar y que estos no corrompan a otras áreas del código que puedan estar usando esa referencia, así al final devolvemos la nueva referencia que acabamos de crear y que es la que será modificada cuando llamemos a esta propiedad computada.

En un mundo ideal esto sería suficiente, los clientes de nuestro código harían uso de las propiedades computadas billedToForRead cuando fueran a leer y billedToForWrite para escribir, pero desgraciadamente no es así y sabemos que mientras más claras las interfaces mejor y también mejoraremos la eficiencia de la propiedad billedToForWrite, sí pues cada vez que usamos esta propiedad efectuamos una nueva copia de Person, cuando lo ideal sería efectuar esta copia solamente si otros objetos mantienen una referencia a este. La versión final del código sería la siguiente:

Comenzaré diciendo que hemos implementado en las dos estructuras y en la clase el protocolo CustomStringConvertible el cual nos permite que cada objeto tenga una representación de tipo texto, que es lo mismo que decir una representación de tipo String y por ende de tipo por valor, esto sería una manera de devolver una interpretación de los elementos del objeto en lugar de retornar una referencia a los mismos, como sería el caso de Bill con su propiedad de tipo Person.

En la estructura Bill es donde más cambios hemos tenido que realizar, demasiados para mi gusto. En el caso de las propiedades computadas billedToForRead y billedToForWrite las hemos establecido como privadas para aislar a los clientes de la estructura del valor por referencia de la clase Person, a partir de esta versión la interacción con Person será a través de las funciones miembro o métodos updateBilledToName y updateBilledToAddress. Como habíamos comentado también hemos mejorado la eficiencia de la propiedad billedToForWrite para evitar que cada vez que usamos esta propiedad efectuamos una nueva copia de Person, para esto nos apoyamos en la función isUniquelyReferencedNonObjC la cual nos permite verificar que billedTo no contenga ninguna referencia fuerte, de ser así podemos modificar su contenido ya que no afectaremos otras áreas del código.

La salida en pantalla es la siguiente:

Antes de finalizar quisiera decir que la implementación de Bill quedó innecesariamente compleja, y su desarrollo ha estado enfocado en demostrar los cuidados que tenemos que tener cuando unimos estos tipos de datos, sin duda alguna el ejemplo trabajado aquí pudo haber sido implementado de una mejor manera.

Antes de finalizas, si en este punto estáis deseosos de conocer más sobre estos temas avanzados pues os recomiendo aprender como se gestiona la memoria en Swift a bajo nivel y que cosa es ARC.

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.

Nota: Este artículo fue inspirado por las siguientes publicaciones:

Reference vs Value Types in Swift: Part 1/2
Reference vs Value Types in Swift: Part 2/2

…ambas escritas por Eric Cerney en idioma ingles y para el sitio web RayWenderlich.