unowned y tests de memoria
Al terminar: Usar unowned cuando la otra referencia vive más y comprobar la memoria con tests.
Al terminar verás con tus ojos qué pasa cuando un unowned falla, tendrás un fichero de tests, ArcTests.swift, que comprueba que el partido se libera cuando nadie lo usa, y habrás provocado a propósito el ciclo para ver fallar esos tests. xcrun swift test dará 95 tests.
Importa porque un ciclo de referencias no avisa de nada: la única señal es que algo no se libera. Un test es la forma de que alguien lo vea por ti, cada vez que ejecutes el paquete.
Temas: unowned frente a weak, el fallo de unowned, un test de liberación con una referencia weak de testigo y los tests que fallan a propósito.
unowned y el fallo que sí se ve
unowned sirve cuando el otro objeto siempre vive al menos tanto como tú: tu hoja de estadísticas (Estadillo) no existe sin su partido. Como no es opcional, te ahorras abrirlo. El riesgo es que, si te equivocas, no obtienes nil: el programa se detiene.
La regla para elegir: si el otro objeto puede desaparecer antes que tú, weak (y tratas el nil). Solo si es imposible que desaparezca antes, unowned. En la duda, weak.
Ejemplo mínimo: un test que mira si algo se liberó
Para saber si un objeto se liberó hace falta alguien que lo mire sin sujetarlo: una variable local weak. Si el objeto se libera, esa variable pasa a nil sola. Es el mismo truco del presentador weak de la lección anterior, pero con una variable local de testigo.
El test crea el partido dentro de un bloque do { }, guarda el testigo y comprueba, al salir del bloque, que vale nil y que deinit se ejecutó. Es uno de los seis que vienen dentro de ArcTests.swift, que copiarás en el paso (no se ejecuta aparte), y la única parte que no has visto es el testigo:
@Test func elPartidoSeLiberaCuandoNadieLoUsa() {
weak var referenciaDebil: PartidoEnVivo? // el testigo: mira sin sujetar, se pone a nil solo
var liberado = false // lo pondrá a true el deinit, por la closure
do {
let partido = PartidoEnVivo(guion: Ejemplo.rayoContraToros, alLiberarse: { liberado = true })
referenciaDebil = partido // el testigo apunta al partido
#expect(referenciaDebil != nil) // dentro del bloque, el partido existe
#expect(!liberado)
} // aquí `partido` sale de su alcance: contador a cero
#expect(referenciaDebil == nil) // el testigo ya no ve nada
#expect(liberado) // deinit se ejecutó
}Si las dos comprobaciones de después del bloque se cumplen, el test pasa; si el partido siguiera vivo (por un ciclo), referenciaDebil no sería nil y el test fallaría.
Reto · Ejercicio: el cliente y su tarjeta
En tu Playground (reutiliza el mismo y borra lo anterior), escribe dos clases. Cliente tiene un nombre y una tarjeta que puede no tener, y la mantiene viva. Tarjeta tiene un número y su cliente: una tarjeta nunca existe sin su cliente, así que lo mira sin mantenerlo vivo y sin que sea opcional. Cada una imprime un aviso al liberarse. En una función, prueba, crea a Ana con la tarjeta 1234, conéctalas e imprime de quién es la tarjeta. Llama a prueba e imprime «fin». Lo que debe salir:
Tarjeta 1234 de Ana
Cliente Ana liberado
Tarjeta 1234 liberada
finDespués, quita la palabra que hace que la tarjeta no mantenga vivo al cliente y mira qué desaparece.
Ver solución
final class Cliente {
let nombre: String
var tarjeta: Tarjeta? // fuerte: el cliente es dueño de su tarjeta
init(nombre: String) { self.nombre = nombre }
deinit { print("Cliente \(nombre) liberado") }
}
final class Tarjeta {
let numero: Int
unowned let cliente: Cliente // unowned: una tarjeta nunca vive más que su cliente
init(numero: Int, cliente: Cliente) {
self.numero = numero
self.cliente = cliente
}
deinit { print("Tarjeta \(numero) liberada") }
}
func prueba() {
let ana = Cliente(nombre: "Ana")
let tarjeta = Tarjeta(numero: 1234, cliente: ana)
ana.tarjeta = tarjeta
print("Tarjeta \(tarjeta.numero) de \(tarjeta.cliente.nombre)")
}
prueba()
print("fin")Salida: las cuatro líneas del enunciado. Sin unowned (con let cliente: Cliente, fuerte) es un ciclo: salen solo Tarjeta 1234 de Ana y fin, y desaparecen las dos líneas de «liberado».
Paso · ArcTests, el aviso de segunda y los fallos a propósito
Objetivo: un fichero de tests que comprueba que el partido se libera y que las dos clases con ciclo no se liberan; arreglar el único aviso que sale; y provocar a propósito dos fallos para verlos.
Dónde trabajas. En el paquete Torneo, abierto en Xcode, con los comandos xcrun swift … ejecutados desde ~/Marcador-Swift/Torneo.
Paso 1 · Los tests de liberación
ArcTests.swift son seis tests. Crea el fichero en la carpeta de tests: clic derecho sobre TorneoTests (dentro de Tests) → New File from Template… → Swift File → Next, escribe ArcTests y pulsa Create (con la Terminal: touch Tests/TorneoTests/ArcTests.swift). Rellénalo con el botón: pulsa Copiar fichero, selecciona todo con Cmd+A y pega con Cmd+V.
No los escribes: el que enseña algo es el del ejemplo de arriba. El fichero incluye también asiNoVale_dosReferenciasFuertesFormanUnCiclo, que usa las dos clases con el bug de la lección anterior (PartidoConCiclo y NarradorConCiclo, declaradas dentro de este mismo fichero) y comprueba que, al salir del bloque, ninguna de las dos se libera.
Ejecuta los tests:
xcrun swift testTermina con una línea como Test run with 95 tests in 12 suites passed (el tiempo varía según la máquina). Si prefieres Xcode, Product → Test (Cmd+U) los ejecuta y el Test navigator (el icono del rombo en el panel de la izquierda, o Cmd+6) lista cada test con su marca verde.
Paso 2 · El aviso de segunda
Compila también los tests para ver sus avisos: xcrun swift build --build-tests. Sale un aviso (warning), no un error: variable 'segunda' was written to, but never read («segunda se escribe pero nunca se lee»), en ArcTests.swift, en la prueba unaSegundaReferenciaFuerteLoMantieneVivo. En Xcode lo ves como un triángulo amarillo en el Issue navigator (el icono del triángulo en el panel de la izquierda, o Cmd+5). La variable segunda sirve para mantener una segunda referencia fuerte y el compilador no lo distingue de un olvido: no sabe que escribirla basta para mantener vivo el objeto. Los tests pasan igual, pero un aviso que se deja puesto tapa los avisos que sí importan, así que se arregla ahora con una línea. Busca la prueba unaSegundaReferenciaFuerteLoMantieneVivo y añade (no sustituyas nada) una línea nueva justo debajo de #expect(referenciaDebil != nil), la que va después de } // contador: 1:
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
} // contador: 1
#expect(referenciaDebil != nil)
#expect(segunda != nil) // NUEVA: leer `segunda` le dice al compilador que se usa
segunda = nil // contador: 0Vuelve a ejecutar xcrun swift build --build-tests: el aviso ya no aparece. La prueba comprueba además, de paso, que la segunda referencia sigue sujetando el partido.
Paso 3 · Provócalo tú
Dos pruebas cortas que enseñan más que leerlas. Después de cada una, deshaz el cambio.
Rompe el weak. En PartidoEnVivo.swift, quita la palabra weak de public weak var partido: PartidoEnVivo? (en Narrador). Ahora las dos referencias son fuertes: es el ciclo de la lección anterior. Ejecuta xcrun swift test: fallan dos tests, unaReferenciaWeakSePoneANilAlLiberarse y conWeakElPartidoYElNarradorSeLiberanJuntos, y la última línea dice Test run with 95 tests in 12 suites failed y cuenta 4 problemas (issues: cada #expect que falla es un problema, y estos dos tests tienen dos cada uno). Es el mismo cambio que hiciste en la lección anterior, pero ahora lo cazan los tests y no tu ojo. Vuelve a escribir weak y comprueba que todo pasa otra vez.
Lee un unowned roto. Añade al final de main.swift:
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
var estadillo: Estadillo?
do {
let partido = PartidoEnVivo(guion: Ejemplo.rayoContraToros)
estadillo = Estadillo(partido: partido)
}
print(estadillo!.golesLocal)Guardas el Estadillo fuera del bloque donde vive su partido y lo lees después. Ejecuta xcrun swift run torneo-cli: el programa se detiene con la línea que ves en el recuadro de «Leer un unowned» de más arriba, con otra dirección, y detrás el informe con Press space to interact…: pulsa cualquier otra tecla para salir. (estadillo! abre el opcional a la fuerza: aquí es solo para provocar el fallo.) Borra esas líneas.
Punto de control
xcrun swift testtermina con una línea comoTest run with 95 tests in 12 suites passed, yxcrun swift build --build-testsya no muestra el aviso desegunda.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.- Provocaste el ciclo: sin
weak, fallan dos tests; conweak, todo vuelve a pasar. Y provocaste el fallo deunownedy borraste las líneas de prueba. - Sabes decir cuándo usar
unownedy cuándoweak.
Comprueba lo que entendiste
4 preguntas · necesitas 3 para completar la lección
1.Lees una referencia
unownedcuyo objeto ya se liberó. ¿Qué ocurre?2.Quitas
weakdeNarrador.partidoy ejecutastorneo-cli. ¿Qué ves?3.
xcrun swift build --build-testsavisa de quesegundase escribe pero nunca se lee. ¿Por qué sale el aviso si el test es correcto?4.Un objeto B vive siempre al menos tanto como A, pero tú no estás del todo seguro. ¿Qué referencia usas desde A hacia B?