ARC
Al terminar: Entender cómo ARC cuenta referencias, y qué son strong, weak y unowned.
Al terminar tendrás en el paquete Torneo una clase PartidoEnVivo que se está jugando, un Narrador que lo cuenta sin mantenerlo vivo y una hoja de estadísticas, Estadillo, que no existe sin su partido. torneo-cli imprimirá PartidoEnVivo liberado (deinit) justo al cerrar un bloque. También sabrás por qué dos objetos que se apuntan entre sí pueden quedarse en memoria para siempre.
Importa porque en Swift la memoria de los objetos la gobierna una regla que tú puedes ver y romper: un objeto vive mientras alguien lo sujete con fuerza. Kotlin te lo esconde. Swift te obliga a decidir quién sujeta a quién.
Temas: ARC, strong, weak, unowned y deinit. La lección siguiente, unowned y tests de memoria, ve qué pasa cuando un unowned falla y comprueba la liberación con tests.
ARC: cada objeto lleva la cuenta de quién lo sujeta
Una referencia es una variable que apunta a un objeto. ARC (Automatic Reference Counting, «recuento automático de referencias») añade a cada instancia de una clase un contador con las referencias fuertes que le apuntan. Cuando el contador llega a cero, Swift libera el objeto en ese mismo instante y ejecuta su deinit.
Hay tres formas de apuntar a un objeto:
| Tipo | Se escribe | ¿Suma al contador? | ¿Es opcional? | Si el objeto ya no existe |
|---|---|---|---|---|
Fuerte (strong) | var narrador: Narrador? (es la forma normal) | Sí | Como quieras | No puede pasar: el objeto sigue vivo |
Débil (weak) | weak var partido: PartidoEnVivo? | No | Sí, siempre, y siempre var | Vale nil automáticamente |
Sin dueño (unowned) | unowned let partido: PartidoEnVivo | No | No | El programa se detiene |
Una clase es un tipo que se pasa por referencia. Un struct o un enum no tienen contador porque se copian. Por eso todo lo de esta lección habla de clases.
Ejemplo mínimo: contar referencias
deinit es un bloque de una clase que Swift ejecuta una sola vez, justo cuando el contador llega a cero. No se llama a mano. Pruébalo en tu Playground, el de Tu primer paquete de Swift: reutilízalo y borra lo anterior antes de pegar cada ejemplo.
final class Balon {
let nombre: String
init(_ nombre: String) { self.nombre = nombre }
deinit { print("\(nombre) liberado") } // corre una vez, cuando el contador llega a 0
}
var a: Balon? = Balon("Nike") // contador: 1
var b = a // contador: 2
a = nil // contador: 1, no pasa nada
print("a vale nil, pero b lo mantiene")
b = nil // contador: 0 → deinit
print("b vale nil")Lo verás en la consola así, en este orden:
a vale nil, pero b lo mantiene
Nike liberado
b vale nilNike liberado sale entre las dos líneas, en el instante en que b suelta el balón. No sale «cuando Swift pueda».
Kotlin y Swift: recolector de basura frente a ARC
| Qué | Kotlin en la JVM (Android) | Swift |
|---|---|---|
| Quién libera | El recolector de basura (garbage collector): busca los objetos a los que ya no se llega desde el programa | ARC: cada objeto cuenta sus referencias fuertes |
| Cuándo | Cuando el recolector pasa; no hay un momento fijo | En el instante en que el contador llega a cero |
| Ciclo A → B → A | Se libera si nadie de fuera llega a ellos | Fuga: ninguno llega a cero |
| Código al liberar | Nada que puedas usar para esto | deinit, en un momento fijo y conocido |
| Fuga típica | Algo alcanzable retiene un Context o una Activity | Un ciclo de referencias fuertes |
Por eso weak y unowned no tienen equivalente diario en Kotlin: existen porque Swift cuenta referencias, y rompen los ciclos que ese recuento no puede resolver.
Las tres referencias juntas
Antes del ejemplo, dos piezas nuevas:
?()al llamar una closure opcional. Una propiedad de tipo(() -> Void)?guarda «una closure sin parámetros que no devuelve nada», onil(lo de() -> Voidya lo viste en Closures). Para llamarla se escribealTerminar?(): el?hace que solo se ejecute si no esnil.do { … }sincatch. Es un bloque suelto: lo que creas dentro (constantes, objetos) deja de existir al cerrar la llave. Sirve para decidir justo dónde termina la vida de una variable.
El ejemplo junta las tres referencias: un concierto, su presentador y una entrada. El guard let concierto else { … } es el que ya conoces de guard let.
final class Concierto {
let artista: String
// Una closure guardada y opcional: () -> Void es «sin parámetros, no devuelve nada».
private let alTerminar: (() -> Void)?
var presentador: Presentador? // fuerte: el concierto mantiene vivo al presentador
init(artista: String, alTerminar: (() -> Void)? = nil) {
self.artista = artista
self.alTerminar = alTerminar
}
deinit {
alTerminar?() // ?(): la llama solo si no es nil
}
}
final class Presentador {
let nombre: String
weak var concierto: Concierto? // weak: no suma al contador; siempre var y opcional
init(nombre: String) { self.nombre = nombre }
func anuncio() -> String {
guard let concierto else { return "\(nombre): no hay concierto" } // nil si ya se liberó
return "\(nombre): con ustedes, \(concierto.artista)"
}
}
final class Entrada {
private unowned let concierto: Concierto // unowned: no suma y no es opcional
init(concierto: Concierto) { self.concierto = concierto }
var artista: String { concierto.artista } // si el concierto ya no existiera, se detendría
}
let presentador = Presentador(nombre: "Rosa")
// do { … } sin catch: un bloque. Lo creado dentro deja de existir al cerrar la llave.
do {
let concierto = Concierto(artista: "Los Grillos", alTerminar: { print("Concierto liberado") })
concierto.presentador = presentador // concierto → presentador: fuerte
presentador.concierto = concierto // presentador → concierto: weak, no hay ciclo
print(Entrada(concierto: concierto).artista)
print(presentador.anuncio())
} // aquí el contador del concierto llega a 0
print(presentador.anuncio()) // weak ya vale nilSe ve así:
Los Grillos
Rosa: con ustedes, Los Grillos
Concierto liberado
Rosa: no hay conciertoEl concierto se libera al cerrar el do, porque lo único que lo apuntaba desde fuera era el weak del presentador.
Error típico: dos referencias fuertes que se apuntan
Tu PartidoEnVivo conocerá a su narrador y el Narrador conocerá a su partido. Si las dos referencias fueran fuertes, cada uno mantendría vivo al otro. Estas dos clases con el bug (PartidoConCiclo y NarradorConCiclo) no las escribirás en tu paquete: en la lección siguiente viven dentro del fichero de tests, para no ensuciarlo.
private final class PartidoConCiclo {
var narrador: NarradorConCiclo?
}
private final class NarradorConCiclo {
var partido: PartidoConCiclo? // fuerte: debería ser `weak`
}Y así queda el arreglo, en una clase mínima: una sola palabra, weak, y el ciclo desaparece. (La versión de verdad de Narrador la escribes en el paso.)
private final class NarradorSinCiclo {
weak var partido: PartidoConCiclo? // weak: no suma al contador
}Para verlo, se crean los dos objetos dentro de un bloque do { }:
do {
let partido = PartidoConCiclo()
let narrador = NarradorConCiclo()
partido.narrador = narrador
narrador.partido = partido
} // las variables locales desaparecen, pero cada objeto sigue sujeto por el otroFalla porque, cuando sales del bloque, las variables locales desaparecen pero cada objeto sigue con contador 1 (lo sujeta el otro): ninguno llega a cero, deinit no corre y la memoria no se recupera. No hay error ni aviso: el programa sigue funcionando. Se corrige preguntando quién es el dueño: el partido es dueño de su narrador, y el narrador solo lo mira, así que el narrador usa weak.
Reto · Ejercicio: el equipo y su utillero
En tu Playground (reutiliza el mismo y borra lo anterior), escribe dos clases, Equipo y Utillero (la persona que guarda el material del equipo), con un nombre cada una, que se conozcan entre sí: el equipo sabe quién es su utillero y el utillero sabe de qué equipo es. Cuando se libera, cada una imprime un aviso con su nombre, como «Equipo Toros liberado». Escribe una función, prueba (dentro de una función porque las variables globales, las que están fuera de cualquier función, viven hasta el final del programa y no se liberan antes), que cree el equipo Toros y el utillero Rosa, los conecte en los dos sentidos e imprima «fin de prueba»; llámala y después imprime «después». Con las dos referencias fuertes, lo que debe salir es:
fin de prueba
despuésAhora haz weak la referencia del utillero hacia su equipo, para que los dos se liberen. Lo que debe salir entonces:
fin de prueba
Equipo Toros liberado
Utillero Rosa liberado
despuésPiensa por qué conviene weak en esa y no en la otra. Pista: un equipo sigue existiendo aunque cambie de utillero; un utillero sin equipo no tiene nada que guardar.
Ver solución
final class Equipo {
let nombre: String
var utillero: Utillero? // el equipo es dueño de su utillero
init(_ nombre: String) { self.nombre = nombre }
deinit { print("Equipo \(nombre) liberado") }
}
final class Utillero {
let nombre: String
weak var equipo: Equipo? // el utillero solo mira: weak rompe el ciclo
init(_ nombre: String) { self.nombre = nombre }
deinit { print("Utillero \(nombre) liberado") }
}
func prueba() {
let e = Equipo("Toros")
let u = Utillero("Rosa")
e.utillero = u
u.equipo = e
print("fin de prueba")
}
prueba()
print("después")Con las dos referencias fuertes, no sale ningún deinit. Con weak en Utillero.equipo, la consola muestra:
fin de prueba
Equipo Toros liberado
Utillero Rosa liberado
despuésCualquiera de las dos weak rompería el ciclo, pero conviene ponerla en la que depende de la otra: aquí el utillero existe para el equipo y no al revés. Si el equipo guardara al utillero con weak, el utillero se liberaría en cuanto nadie más lo sujetara, aunque el equipo lo siga necesitando (y las dos líneas de «liberado» podrían salir en otro orden).
Paso · PartidoEnVivo, el narrador y el estadillo
Objetivo: una clase PartidoEnVivo con un narrador, una hoja de estadísticas (Estadillo) y un bloque en torneo-cli que demuestra que el partido se libera al salir de su bloque.
Dónde trabajas. En el paquete Torneo, abierto en Xcode. Los comandos xcrun swift … de esta lección se ejecutan todos desde ~/Marcador-Swift/Torneo, la carpeta donde está Package.swift.
Paso 1 · Los datos de ejemplo: Ejemplo.swift
Ejemplo.swift son datos de ejemplo (jugadores, equipos y dos guiones de partido) que la consola y los tests usan tal cual. No enseña nada de memoria, así que no lo escribes: lo copias. Usa dos piezas que todavía no has escrito:
static y el enum sin casos. Una propiedad static pertenece al tipo, no a una instancia: se lee como Ejemplo.ana, sin crear ningún Ejemplo. Un enum sin casos no se puede instanciar, así que es la forma habitual de agrupar constantes y funciones bajo un nombre (un espacio de nombres). El equivalente en Kotlin es object Ejemplo.
try!, que ya escribiste en Errores: throws, try y do/catch: promete que no va a fallar y, si falla, el programa se detiene. Aquí vale porque son datos escritos a mano: si estuvieran mal, se vería al primer arranque. Y Equipo(…)!: el init de Equipo puede fallar y por eso devuelve un opcional (Equipo?); el ! lo abre a la fuerza, como en Struct y class. Aquí los datos son válidos, así que nunca será nil.
- Crea el fichero. En el Project navigator, clic derecho sobre la carpeta
Torneoque está dentro deSources(no sobre la deTests) → New File from Template… → Swift File → Next, escribe el nombreEjemploy pulsa Create. En un paquete Swift el fichero pertenece al objetivoTorneopor estar en su carpeta: no hay que marcar ningún objetivo. Si el menú de tu versión de Xcode se llama distinto, crea el fichero desde la Terminal contouch Sources/Torneo/Ejemplo.swifty Xcode lo muestra solo. - Pulsa el botón de abajo. Aparecerá un aviso de que se ha copiado.
- Haz clic dentro del editor con el fichero abierto, selecciona todo con Cmd+A y pega con Cmd+V. Lo pegado reemplaza la plantilla que Xcode pone en un fichero nuevo.
Puedes ojearlo: son static let con jugadores, equipos y dos partidos de ejemplo, Ejemplo.rayoContraToros y Ejemplo.lobosContraAguilas.
Paso 2 · La clase PartidoEnVivo, Narrador y Estadillo
Crea PartidoEnVivo.swift igual que antes: clic derecho sobre la carpeta Torneo de Sources → New File from Template… → Swift File → Next, escribe PartidoEnVivo y pulsa Create.
Aquí no hay piezas nuevas: todo está en el ejemplo del concierto y en lo que ya escribiste. Solo recuerda tres cosas:
public final class:public, como en todas tus clases deTorneo, la hace visible desdetorneo-cli;final, que nadie hereda de ella.weak var partido: PartidoEnVivo?yunowned let partido: PartidoEnVivoson las dos referencias sin dueño de la tabla de arriba, yalLiberarse: (() -> Void)?es la closure que se llama desdedeinit, como la deConcierto. Sirve para que la consola vea eldeinit.- Nombres exactos que usan
main.swifty los tests de la lección siguiente:PartidoEnVivose crea con las etiquetasguionyalLiberarse(esta, opcional) y tiene la propiedadnarrador;Narradorse crea connombrey tienepartidoyfrase;Estadillose crea conpartidoy tienegolesLocal. La frase del narrador tiene la forma «Pepe: Rayo FC contra Toros», con los nombres de los equipos del guion.
Intenta escribirlo tú, en PartidoEnVivo.swift, tres clases públicas de las que nadie hereda:
PartidoEnVivo, con su guion (unPartido), un narrador opcional al que mantiene vivo, y una closure opcional que recibe al crearse y que llama justo cuando se libera.Narrador, con su nombre y el partido que narra, mirado sin mantenerlo vivo; su frase dice «Pepe: Rayo FC contra Toros» o, si el partido ya no existe, «Pepe: ya no hay partido».Estadillo, que guarda su partido sin mantenerlo vivo ni tratarlo como opcional (un estadillo no existe sin su partido), y da los goles del local.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// Un partido que se está jugando. `guion` es todo lo que va a pasar.
//
// Es una CLASE, y no un struct como `Partido`, porque tiene IDENTIDAD y vida:
// alguien lo empieza, varios lo miran y, cuando ya nadie lo necesita, se libera.
// ARC (Automatic Reference Counting) cuenta cuántas referencias FUERTES apuntan
// a cada objeto y lo libera cuando llegan a cero. Los structs y enums no lo
// necesitan: se copian.
public final class PartidoEnVivo {
public let guion: Partido
// Se llama en `deinit`, es decir, justo cuando ARC libera el objeto. Sirve
// para que las pruebas (y la consola) vean que de verdad se liberó.
private let alLiberarse: (() -> Void)?
// `strong` es lo normal: esta referencia mantiene vivo al narrador.
public var narrador: Narrador?
public init(guion: Partido, alLiberarse: (() -> Void)? = nil) {
self.guion = guion
self.alLiberarse = alLiberarse
}
// `deinit` corre UNA vez, cuando el contador de referencias fuertes llega a
// cero. No se llama a mano. (Kotlin no tiene nada igual: el recolector de
// basura libera «cuando puede», y no hay un momento fijo.)
deinit {
alLiberarse?()
}
}
// El narrador cuenta el partido. Necesita saber de qué partido habla, y el
// partido sabe quién lo narra: dos objetos que se apuntan entre sí.
//
// Si las DOS referencias fueran fuertes, ninguno de los dos llegaría nunca a
// contador cero (cada uno mantiene vivo al otro) y no se liberarían jamás:
// un ciclo de retención (retain cycle). La solución es que UNA de las dos sea
// `weak`: no suma al contador y se pone a `nil` cuando el objeto se libera.
// La pregunta es «¿quién es el dueño?». El partido es el dueño del narrador,
// no al revés, y por eso el narrador lo mira con `weak`.
public final class Narrador {
public let nombre: String
public weak var partido: PartidoEnVivo?
public init(nombre: String) {
self.nombre = nombre
}
public func frase() -> String {
// Como `partido` es `weak`, es opcional: hay que abrirlo.
guard let partido else { return "\(nombre): ya no hay partido" }
return "\(nombre): \(partido.guion.local.nombre) contra \(partido.guion.visitante.nombre)"
}
}
// `unowned`: como `weak` pero NO opcional. Se usa cuando el otro objeto siempre
// vive al menos tanto como éste: un estadillo no existe sin su partido. Si te
// equivocas y el partido ya no está, el programa se detiene (a diferencia de
// `weak`, que te da `nil`). Es más cómodo y más peligroso.
public final class Estadillo {
private unowned let partido: PartidoEnVivo
public init(partido: PartidoEnVivo) {
self.partido = partido
}
public var golesLocal: Int { partido.guion.golesLocal }
}Paso 3 · Soltar el partido en main.swift
Abre main.swift (en Sources, dentro de torneo-cli). Al final del fichero, después del último } de lo que ya tienes, intenta escribir un bloque como el del ejemplo del concierto, cuyas constantes desaparecen al cerrar la llave:
- Dentro del bloque, crea el partido en vivo de Rayo contra Toros, que al liberarse imprime «PartidoEnVivo liberado (deinit)».
- Crea a Pepe como narrador y enlázalos en los dos sentidos.
- Imprime la frase del narrador y avisa de que al salir se libera el partido.
- Después del bloque, escribe una última línea.
Lo que debes ver al ejecutarlo, al final de la salida:
Pepe: Rayo FC contra Toros
Al salir del bloque, ARC libera el partido...
PartidoEnVivo liberado (deinit)
...y ya no hay nada que narrar.Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// ARC: el partido se libera cuando nadie lo mira.
do {
let enVivo = PartidoEnVivo(guion: Ejemplo.rayoContraToros, alLiberarse: { print("PartidoEnVivo liberado (deinit)") })
let narrador = Narrador(nombre: "Pepe")
enVivo.narrador = narrador
narrador.partido = enVivo
print(narrador.frase())
print("Al salir del bloque, ARC libera el partido...")
}
print("...y ya no hay nada que narrar.")Ejecútalo:
xcrun swift run torneo-cliLa primera vez compila (tarda unos segundos). Las cuatro últimas líneas son las del bloque:
Pepe: Rayo FC contra Toros
Al salir del bloque, ARC libera el partido...
PartidoEnVivo liberado (deinit)
...y ya no hay nada que narrar.Si prefieres Xcode: arriba, en el centro de la barra de herramientas, el selector de esquema (scheme) debe decir torneo-cli y el destino (destination), My Mac; Product → Run (Cmd+R) lo ejecuta y la salida aparece en la consola, el panel de abajo (el Debug area; se muestra con Cmd+Shift+Y).
Paso 4 · Rompe el weak
Una prueba corta que enseña más que leerla. En PartidoEnVivo.swift, quita la palabra weak de public weak var partido: PartidoEnVivo? (en Narrador). Ahora las dos referencias son fuertes: es el ciclo del error típico. Ejecuta xcrun swift run torneo-cli: la línea PartidoEnVivo liberado (deinit) ya no sale, y no hay ningún error ni aviso. Vuelve a escribir weak y comprueba que la línea reaparece.
Punto de control
xcrun swift run torneo-clitermina con estas cuatro líneas, en este orden:Pepe: Rayo FC contra Toros,Al salir del bloque, ARC libera el partido...,PartidoEnVivo liberado (deinit)y...y ya no hay nada que narrar.xcrun swift testsigue enTest run with 89 tests in 11 suites passed, yxcrun swift build --build-testsno muestra ningún aviso.- Quitaste el
weak, viste desaparecer eldeinitsin ningún error y lo volviste a poner. - Sabes decir por qué
Narrador.partidoesweaky no fuerte.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.¿Cuándo se ejecuta el
deinitde una instancia de una clase?2.¿Cómo tiene que declararse una referencia
weak?3.Un
PartidoConCicloguarda una referencia fuerte a suNarradorConCiclo, y el narrador guarda una referencia fuerte al partido. Sales del bloque donde creaste los dos. ¿Qué pasa?4.Dos objetos de Kotlin en la JVM se apuntan entre sí y nadie más llega a ellos. ¿Por qué no hace falta un
weakcomo en Swift?5.En el ejemplo del balón, ¿cuándo sale
Nike liberado?