Saltar al contenido
K
Swift: memoria y concurrenciaPaso a paso28 min de lectura · 5 min de máquina Intermedio

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:

ejemplo mínimo: el test que enseña algo (con comentarios)
    @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
fin

Después, quita la palabra que hace que la tarjeta no mantenga vivo al cliente y mira qué desaparece.

Ver solución
solución: el cliente y su tarjeta
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.

Torneo
Tests
TorneoTests
ArcTests.swiftse crea y se rellena con «Copiar fichero»
Un fichero nuevo que se copia. Así lo ves en el Project navigator.

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.

ArcTests.swift

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 test

Termina 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
ArcTests.swift: la línea nueva está marcada
        }   // contador: 1
        #expect(referenciaDebil != nil)
        #expect(segunda != nil)   // NUEVA: leer `segunda` le dice al compilador que se usa
        segunda = nil   // contador: 0

Vuelve 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
líneas de prueba para main.swift (bórralas después)
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 test termina con una línea como Test run with 95 tests in 12 suites passed, y xcrun swift build --build-tests ya no muestra el aviso de segunda.
  • xcrun swift run torneo-cli termina 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; con weak, todo vuelve a pasar. Y provocaste el fallo de unowned y borraste las líneas de prueba.
  • Sabes decir cuándo usar unowned y cuándo weak.

Comprueba lo que entendiste

4 preguntas · necesitas 3 para completar la lección

  1. 1.Lees una referencia unowned cuyo objeto ya se liberó. ¿Qué ocurre?

  2. 2.Quitas weak de Narrador.partido y ejecutas torneo-cli. ¿Qué ves?

  3. 3.xcrun swift build --build-tests avisa de que segunda se escribe pero nunca se lee. ¿Por qué sale el aviso si el test es correcto?

  4. 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?

Faltan 4 de 4