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 weak
, o hacerlo mediante unowned
.
Una referencia unowned
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 weak
, ¿no?
Entonces, ¿qué diferencia existe entre crear una referencia débil con weak
o crearla mediante unowned
?
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 weak
en la propiedad author
de la clase Book
.
Sin embargo, el caso de uso de unowned
es algo diferente.
Las referencias unowned
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 Customer
y CreditCard
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 unowned
SIEMPRE se espera que tenga un valor. O dicho de otro modo, nunca tendrá el valor nil
. 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 Customer
y CreditCard
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 Customer
y CreditCard
es ligeramente diferente a la que teníamos con Author
y Book
.
En el ejemplo del artículo anterior, Author
puede tener su propiedad book
con valor nil
si éste todavía no ha publicado ningún libro; a su vez, Book
puede tener su propiedad author
a nil
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 Customer
tiene una propiedad card
opcional, y la clase CreditCard
tiene una propiedad customer,
unowned
y no opcional. Fíjate además, que la clase CreditCard
tiene un init
que recibe el Customer
, 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 retainCount
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 Customer
, comienza la asignación a nil
de todas sus propiedades, por lo que se rompe también la referencia fuerte que apuntaba hacia CreditCard
. En ese momento, su retainCount
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 Customer
, también lo hace la de CreditCard
. Se ejecutan los métodos deinit
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 unowned
, pero éste ya ha sido eliminado de memoria, se provoca un crash en tu aplicación.
Recuerda que las propiedades unowned
no se declaran de tipo opcional y no pueden ser nil
en tiempo de ejecución. Si eso ocurre e intentas acceder a ella, el crash es inevitable.
¿Cuándo utilizar unowned
?
Utiliza unowned
en lugar de weak
sólo cuando estés seguro que el ciclo de memoria del objeto B
es igual (o mayor) que el del objeto A
, y que por tanto la referencia unowned
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 weak
o unowned
.
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 Author
y Book
muestra la situación en donde dos propiededes, las cuales pueden tener el valor de nil
, crean una referencia circular fuerte. Este escenario se resuelve mejor con weak
.
El ejemplo de Customer
y CreditCard
muestra una situación donde una de las propiedades puede tener el valor nil
, pero la la otra propiedad no puede ser nil
en ningún momento. Esta escenario se resuelve mejor con unowned
.
Si estás empezando, te recomiendo utilizar por ahora weak
, 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.
0 comentarios