fbpx

Alexandre Freire

Blog sobre desarrollo en Swift, iOS y Xcode

Unowned vs Weak en Swift – Gestión de memoria en Swift (Parte 3)

Feb 18, 2019

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.

¡YEEES!

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í).

Whaaaaat?

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í:

ObjectretainCount
Customer1
CreditCard1

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.

ObjectretainCount
Customer0
CreditCard0

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 😝.

¡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.