Este es el tercer episodio de la serie «Manejo de memoria en Swift». En el primero vimos qué era ARC y cómo funcionaba. En el segundo, descubrimos qué es una referencia circular fuerte entre dos clases y cómo podemos evitarlo.
En esta parte 3 de la serie, vamos a ver cuál es la diferencia entre crear una referencia débil con <strong>weak</strong>
, o hacerlo mediante <strong>unowned</strong>
.
Una referencia <strong>unowned</strong>
sirve para romper referencias circulares. Transforma una referencia fuerte en una débil que no incrementa en +1 la cuenta de referencias de la instancia.
De momento, hace lo mismo que <strong>weak</strong>
, ¿no?
Entonces, ¿qué diferencia existe entre crear una referencia débil con <strong>weak</strong>
o crearla mediante <strong>unowned</strong>
?
Referencias unowned
(unowned references):
Las unowned
references crean una referencia débil que no suma +1 a la cuenta de referencia de la instancia a la que apuntan, tal y como hicimos en el artículo anterior cuando utilizamos <strong>weak</strong>
en la propiedad <strong>author</strong>
de la clase <strong>Book</strong>
.
Sin embargo, el caso de uso de <strong>unowned</strong>
es algo diferente.
Las referencias <strong>unowned</strong>
son utilizadas cuando la instancia que tiene la referencia débil tiene el mismo ciclo de vida que la otra instancia. A efectos prácticos, esto significa que el objeto B
siempre va a estar en memoria mientras el objeto A
también lo esté (y además, no tiene sentido que no sea así).
Imagina que estás creando una tienda online, y tienes los objetos <strong>Customer</strong>
y <strong>CreditCard</strong>
para modelar a tus clientes y sus tarjetas de crédito, respectivamente. ¿Tiene sentido tener una tarjeta de crédito en memoria si el cliente ya ha sido eliminado? No. Tan pronto se elimine el cliente, no necesitas para nada su tarjeta y ésta también debe eliminarse de memoria.
Por este motivo, una referencia <strong>unowned</strong>
SIEMPRE se espera que tenga un valor. O dicho de otro modo, nunca tendrá el valor <strong>nil</strong>
. Esto se traduce en que las unowned
references se declaran sin utilizar tipos opcionales en Swift.
¿Qué significa que dos objetos tengan el mismo ciclo de vida?
Significa que, si el objeto B
existe en memoria, también lo hace el objeto A
. Y que no tiene ningún sentido que sea al contrario.
En estos casos, para romper una referencia circular utilizamos unowned
en lugar de weak
.
¿Qué ganamos con esto?
- Sobre todo, evitamos tener que utilizar opcionales, que siempre «ensucian» un poco el código.
- Además, nos aporta más información acerca del ciclo de vida del objeto y sus relaciones. D
Volvamos al ejemplo de la tienda online. Tenemos las clases <strong>Customer</strong>
y <strong>CreditCard</strong>
para modelar a los clientes y una posible tarjeta de crédito.
Ambas clases tienene una propiedad apuntando hacia la otra, por lo que podríamos estar creando una referencia circular.
Sin embargo, la relación entre <strong>Customer</strong>
y <strong>CreditCard</strong>
es ligeramente diferente a la que teníamos con <strong>Author</strong>
y <strong>Book</strong>
.
En el ejemplo del artículo anterior, <strong>Author</strong>
puede tener su propiedad <strong>book</strong>
con valor <strong>nil</strong>
si éste todavía no ha publicado ningún libro; a su vez, <strong>Book</strong>
puede tener su propiedad <strong>author</strong>
a <strong>nil</strong>
si se trata de un libro de autor anónimo.
En el ejemplo de la tienda online, un cliente puede tener (o no) una tarjeta de crédito asociada, pero una tarjeta de crédito debe estar y estará siempre asociada a un cliente. O lo que es lo mismo, una tarjeta de crédito no existirá nunca si no lo hace el cliente.
Para representar esta relación, la clase <strong>Customer</strong>
tiene una propiedad <strong>card</strong>
opcional, y la clase <strong>CreditCard</strong>
tiene una propiedad <strong>customer,</strong>
<strong>unowned</strong>
y no opcional. Fíjate además, que la clase <strong>CreditCard</strong>
tiene un <strong>init</strong>
que recibe el <strong>Customer</strong>
, ya que no puede existir una tarjeta sin cliente.
class Customer {
// MARK: - Properties
let name: String
var card: CreditCard?
// MARK: - Initialization
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized")
}
}
class CreditCard {
// MARK: - Properties
let number: UInt64
unowned let customer: Customer
// MARK: - Initialization
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit {
print("Card \(number) is being deinitialized")
}
}
A continuación vamos a crear un objeto de tipo Customer?
, para poder asignarlo después a nil
, y un objeto CreditCard
.
var alexandre: Customer? = Customer(name: "Alexandre Freire")
alexandre?.card = CreditCard(number: 1234_5678_9012_3456, customer: alexandre!)
El layout de memoria ahora mismo luce así:
La instancia Customer
tiene una referencia fuerte hacia la instancia CreditCard
, y ésta tiene una referencia unowned
(que no incrementa la cuenta de referencias) hacia la de Customer
.
Ahora mismo, la traza del <strong>retainCount</strong>
de cada instancia estaría así:
Object | retainCount |
Customer | 1 |
CreditCard | 1 |
Si rompemos la referencia fuerte existente entre el símbolo alexandre
y la instancia Customer
(asignándola a nil
) su cuenta de referencias llega a cero, ya que no hay más referencias fuertes apuntando hacia la instancia y por tanto se elimina de memoria.
alexandre = nil
// Se imprime por consola:
// "Alexandre Freire is being deinitialized"
// "Card #1234567890123456 is being deinitialized"
En el momento en que se elimina de memoria la instancia de <strong>Customer</strong>
, comienza la asignación a <strong>nil</strong>
de todas sus propiedades, por lo que se rompe también la referencia fuerte que apuntaba hacia <strong>CreditCard</strong>
. En ese momento, su <strong>retainCount</strong>
llega también a cero y se elimina de memoria.
Object | retainCount |
Customer | 0 |
CreditCard | 0 |
Fíjate que tan pronto se elimina de memoria la instancia de <strong>Customer</strong>
, también lo hace la de <strong>CreditCard</strong>
. Se ejecutan los métodos <strong>deinit</strong>
de las dos instancias. Justo lo que se supone que tiene que pasar, en este caso 😝.
Únete a la newsletter para no perderte ningún nuevo tutorial: recibe un email cuando se publiquen nuevos artículos o vídeos y no pierdas la oportunidad de seguir aprendiendo.
¡Cuidado! ¡Crash a la vista!
Hay que tener especial cuidado con las referencias unowned.
Si intentas acceder a un objeto mediante una propiedad <strong>unowned</strong>
, pero éste ya ha sido eliminado de memoria, se provoca un crash en tu aplicación.
Recuerda que las propiedades <strong>unowned</strong>
no se declaran de tipo opcional y no pueden ser <strong>nil</strong>
en tiempo de ejecución. Si eso ocurre e intentas acceder a ella, el crash es inevitable.
¿Cuándo utilizar unowned
?
Utiliza <strong>unowned</strong>
en lugar de <strong>weak</strong>
sólo cuando estés seguro que el ciclo de memoria del objeto <strong>B</strong>
es igual (o mayor) que el del objeto <strong>A</strong>
, y que por tanto la referencia <strong>unowned</strong>
apunta a un objeto que no ha sido eliminado de memoria todavía.
Conclusión
Para romper referencias circulares fuertes entre dos tipos por referencia podemos utilizar <strong>weak</strong>
o <strong>unowned</strong>
.
Aunque internamente sirven para lo mismo, que es crear una referencia débil que no aumenta en +1 la cuenta de referencias de la instancia, tienen características y casos de uso diferentes.
El ejemplo de <strong>Author</strong>
y <strong>Book</strong>
muestra la situación en donde dos propiededes, las cuales pueden tener el valor de <strong>nil</strong>
, crean una referencia circular fuerte. Este escenario se resuelve mejor con <strong>weak</strong>
.
El ejemplo de <strong>Customer</strong>
y <strong>CreditCard</strong>
muestra una situación donde una de las propiedades puede tener el valor <strong>nil</strong>
, pero la la otra propiedad no puede ser <strong>nil</strong>
en ningún momento. Esta escenario se resuelve mejor con <strong>unowned</strong>
.
Si estás empezando, te recomiendo utilizar por ahora <strong>weak</strong>
, ya que es más seguro al no provocar crashes en tu aplicación. Comienza a utilizar unowned
cuando ganes experiencia y tengas claro que tus dos objetos tienen el mismo ciclo de memoria y que, por tanto, nunca accederás a una instancia que ha sido eliminada de memoria.