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

Sendable y el modo Swift 6

Al terminar: Cambiar el modo de lenguaje del paquete a Swift 6, marcar los tipos como Sendable y escribir el estado de pantalla en el actor principal, sin avisos.

Al terminar esta lección, el paquete Torneo compila en modo Swift 6 sin un solo aviso y Equipo puede viajar entre tareas sin riesgo. Hasta la lección anterior el compilador te dejaba pasar los peligros de la concurrencia; ahora los convierte en errores, y tú los arreglas uno a uno con los mensajes literales delante.

Importa porque esta es la comprobación que convierte el data race de Actores (un número mal, a veces) en un error de compilación que se ve antes de ejecutar. Temas: Sendable, Mutex y la concurrencia estricta de Swift 6. El actor principal, que se usa de verdad en la lección siguiente, se explica en @MainActor y el hilo principal.

Sendable: lo que puede cruzar de una tarea a otra

Un dominio de concurrencia es una zona con sus propias reglas de acceso: un actor, el actor principal o una tarea. Sendable es el protocolo que dice «este tipo se puede pasar de un dominio a otro sin riesgo». El compilador lo comprueba:

Tipo¿Es Sendable?
struct o enum cuyos campos son SendableSí, aunque los campos sean var: al cruzar, se copia. En un tipo public hay que escribirlo: no se deduce
actorSí, siempre: protege su estado
final class con solo let SendableSí, si escribes : Sendable: en las clases nunca se deduce
class con un var que cambiaNo: dos tareas podrían pisarse (el caso de MarcadorInseguro)
Una closure que cruza tareasDebe marcarse @Sendable, y no puede capturar un var que cambia. Las closures de Task { } y addTask { } ya lo son: solo la marcas tú en las que guardas o pasas a mano
ejemplo mínimo: Sendable (con comentarios)
struct Gol: Sendable {          // un struct: se copia al cruzar entre tareas
    var minuto: Int             // `var` vale: cada tarea recibe su copia y cambia la suya
    let jugador: String         // `let` también vale: no cambia nunca
}
 
final class Cuenta: Sendable {  // una clase: todas las tareas comparten EL MISMO objeto
    var total = 0               // error: stored property 'total' of 'Sendable'-conforming class 'Cuenta' is mutable
                                // (con `let` compilaría)
}

Pega el código en tu Playground y verás el error en rojo en la línea de total: es el modo Swift 6, y es el mismo mensaje que da el paquete.

El modo Swift 6

Un modo de lenguaje es el conjunto de reglas que aplica el compilador. Con swift-tools-version: 6.2 el modo por defecto es Swift 6. Desde Closures el paquete tenía swiftLanguageModes: [.v5], que desactivaba casi todas estas comprobaciones para poder aprender cada tema por separado. El compilador es el mismo en los dos modos: lo que cambia es cuántas cosas rechaza.

Cuando el compilador encuentra algo que Swift 6 no permite, lo cuenta de una de estas dos maneras, según el modo: en Swift 5 con la comprobación estricta encendida (el recuadro de más abajo explica cómo se enciende) es un aviso (warning: el programa compila igual) y en Swift 6 es un error (el programa no compila). Es la misma frase en los dos casos; en el aviso acaba con this is an error in the Swift 6 language mode (en el modo Swift 6 esto es un error). Esa escalera es lo que hay detrás del ajuste que cambias en el primer paso.

Dos salidas para una clase que cambia: Mutex o @unchecked

Para que un struct sea Sendable, todo lo que guarda tiene que serlo. Una clase con un valor que cambia no lo es, y en el paquete la tienes: Equipo guarda un capitan que cambia. Tiene dos salidas:

SalidaQué hacePor qué
@unchecked SendableLe dices al compilador «confía, yo me hago responsable»Se calla, y el bug sigue ahí: es justo lo que no hay que hacer
MutexEl valor vive dentro de un cerrojo (una pieza que deja pasar a una sola tarea a la vez): solo una tarea a la vez ejecuta el bloque de withLockEl compilador puede demostrar que es seguro

Se usa Mutex (de import Synchronization). Necesita macOS 15 e iOS 18, y tu platforms ya pide justo eso desde async/await (some AsyncSequence<String, Never> y Task.sleep(for:) ya lo exigían). No hay nada que instalar: Synchronization viene con Swift y con el sistema.

Rompe la simetría con Kotlin. El Equipo de Kotlin tiene un var capitan con private set y ningún cerrojo: la JVM deja pasar cualquier objeto entre hilos, y tiene el mismo riesgo sin que nadie lo avise. En Swift, para poder cruzar tareas, el dato que cambia entra en un Mutex, que bloquea el hilo un instante mientras alguien lo usa (por eso el bloque de withLock debe ser corto).

Piezas nuevas de esta lección:

  • Sendable, @Sendable y @unchecked Sendable: los de arriba.
  • import Synchronization y Mutex<Jugador?>(nil): un cerrojo que guarda un valor (aquí, un capitán opcional); withLock { … } da acceso al valor dentro del bloque, con $0 como nombre del valor (los parámetros anónimos de Closures): { $0 } lo lee y { $0 = jugador } lo cambia (llega como inout, un parámetro que se puede modificar).
  • 1_000: el guion bajo solo separa los miles para leerlo mejor; vale igual que 1000.
  • (@Sendable () -> Void)?: una closure @Sendable que puede ser nil; los paréntesis son necesarios para que el ? afecte a toda la closure.

Ya las viste: la propiedad calculada (public var capitan: Jugador? { … }, sin valor guardado: se calcula cada vez que se lee) viene de Propiedades, y el Task y el TaskGroup de los ejemplos, de async/await y TaskGroup y errores en tareas hijas.

El Mutex y la closure @Sendable opcional, con una taquilla en vez de un equipo:

ejemplo mínimo: una clase Sendable con Mutex y withLock (con comentarios)
import Synchronization                        // trae Mutex; viene con Swift, no se instala
 
public final class Taquilla: Sendable {        // una clase Sendable: solo let y nada que cambie sin cerrojo
    public let nombre: String
    // El valor que cambia vive dentro del cerrojo. Mutex<Int>: guarda un Int.
    private let vendidas = Mutex<Int>(0)
 
    public init(nombre: String) { self.nombre = nombre }
 
    // Propiedad calculada: lee el valor dentro del cerrojo. { $0 } devuelve lo que hay.
    public var entradasVendidas: Int { vendidas.withLock { $0 } }
 
    public func vender() {
        vendidas.withLock { $0 += 1 }          // $0 llega como inout: aquí sí se puede cambiar
    }
}
 
let taquilla = Taquilla(nombre: "Norte")
await withTaskGroup(of: Void.self) { grupo in
    for _ in 0..<1_000 {
        grupo.addTask { taquilla.vender() }    // compila en Swift 6: Taquilla es Sendable
    }
}
print(taquilla.entradasVendidas)               // 1000, siempre
 
// Una closure @Sendable que puede ser nil: los paréntesis hacen opcional toda la closure.
let alCerrar: (@Sendable () -> Void)? = { print("taquilla cerrada") }
alCerrar?()                                    // el ? solo la llama si no es nil
1000
taquilla cerrada

Reto · Ejercicio: un sensor que cruzan mil tareas

En tu Playground (borra lo anterior), escribe un struct, Lectura, con un valor entero que se pueda cambiar y que se pueda compartir entre tareas. Escribe también una clase final, Sensor, compartible entre tareas sin promesas sin comprobar, que cuente cuántas lecturas recibe y recuerde la mayor (lo que cambia va en un cerrojo).

Lanza a la vez mil tareas que registran las lecturas del 1 al 1000 y, al terminar, imprime el número de lecturas y la máxima. Lo que debe salir:

Lecturas: 1000
Máxima: 1000

Para pensar: ¿qué error da si guardas el resumen en una propiedad normal que se pueda cambiar, sin cerrojo?

Ver solución
solución: Sendable con Mutex
import Synchronization
 
struct Lectura: Sendable { var valor: Int }      // struct con var: Sendable, se copia al cruzar
 
struct Resumen { var cuantas = 0; var maxima = 0 }   // solo enteros: Sendable sin escribirlo (no es public)
 
final class Sensor: Sendable {
    private let guardado = Mutex(Resumen())      // lo que cambia vive en el cerrojo
    func registrar(_ lectura: Lectura) {
        guardado.withLock {
            $0.cuantas += 1
            if lectura.valor > $0.maxima { $0.maxima = lectura.valor }   // se queda con la mayor
        }
    }
    var resumen: Resumen { guardado.withLock { $0 } }
}
 
let sensor = Sensor()
await withTaskGroup(of: Void.self) { grupo in
    for n in 1...1_000 {
        grupo.addTask { sensor.registrar(Lectura(valor: n)) }
    }
}
print("Lecturas: \(sensor.resumen.cuantas)")
print("Máxima: \(sensor.resumen.maxima)")

Lectura es Sendable con var porque al cruzar entre tareas se copia, y cada tarea cambia su copia. Sensor es una clase: las mil tareas comparten el mismo objeto, así que lo que cambia necesita un cerrojo. Con una propiedad var normal en lugar del Mutex, el error es stored property '…' of 'Sendable'-conforming class 'Sensor' is mutable, el mismo de Cuenta en el ejemplo de arriba. Es lo que le pasa a Equipo con su capitán en el segundo paso.

Paso · Cambia el modo de lenguaje a Swift 6

Objetivo: quitar el modo Swift 5 del Package.swift y ver, con los mensajes reales, todo lo que Swift 6 te va a exigir. Al terminar, xcrun swift build --build-tests falla con una lista de errores de concurrencia: es lo que esperas ver, y los arreglas en los pasos siguientes. Es el ajuste que pusiste en Closures, deshecho: allí está la receta completa del manifiesto.

Torneo
Package.swiftse modifica: una línea
El único fichero que tocas en este paso, en el panel izquierdo de Xcode (el Project navigator) con el paquete Torneo abierto.

1 · Qué ajustas y por qué ahora. El modo de lenguaje del paquete, la propiedad swiftLanguageModes de Package.swift, que pusiste en .v5 para aprender un tema cada vez. Ya conoces actores y tareas, las piezas que Swift 6 comprueba: es el momento de quitar el apagón.

2 · Dónde vive. En Package.swift, el manifiesto del paquete (lo creó Tu primer paquete de Swift): en Xcode, Torneo → Package.swift en el panel izquierdo. Ahí hay dos cosas que se parecen:

  • La primera línea, // swift-tools-version: 6.2: la versión de herramientas que necesita el manifiesto. No la tocas.
  • swiftLanguageModes, justo antes del paréntesis que cierra Package(...): el modo con el que se compila el código de tus ficheros .swift. Es la que cambias.

3 · El cambio. Borra la línea swiftLanguageModes: [.v5] y la coma que hay después del ] que cierra targets: el último argumento no lleva coma.

Package.swift: el final, antes
    ],
    swiftLanguageModes: [.v5]
)
Package.swift: el final, después
    ]
)

Guarda el fichero con Cmd+S. Sin la línea, el paquete vuelve al modo que le toca por su versión de herramientas: Swift 6.

4 · Compila (aquí no hay «Sync Now» como en Android Studio: Swift lee el manifiesto cada vez que compilas). En la Terminal:

cd ~/Marcador-Swift/Torneo
xcrun swift build --build-tests

--build-tests compila también los tests (se usa en los pasos siguientes). Esta primera vez la compilación se detiene en el módulo Torneo y no llega a los tests. Verás más de veinte errores; estos son cuatro, uno de cada tipo (las líneas y columnas dependen de los comentarios que hayas escrito; los dos primeros son iguales en todos los paquetes):

Sources/Torneo/Closures.swift:4:12: error: let 'esGol' is not concurrency-safe because non-'Sendable' type '(EventoDePartido) -> Bool' may have shared mutable state [#MutableGlobalVariable]
Sources/Torneo/Ejemplo.swift:6:23: error: static property 'ana' is not concurrency-safe because non-'Sendable' type 'Jugador' may have shared mutable state [#MutableGlobalVariable]
Sources/Torneo/PartidoEnVivo.swift:57:17: error: capture of 'alTerminar' with non-Sendable type '(() -> Void)?' in a '@Sendable' closure [#SendableClosureCaptures]
Sources/Torneo/Simulacion.swift:99:15: error: type 'Partido' does not conform to the 'Sendable' protocol

Si tienes el paquete abierto en Xcode, también los verás en el panel de Issues (el triángulo con una exclamación, arriba en el panel izquierdo) tras Product → Build (Cmd+B). El esquema (scheme, la lista desplegable de la barra superior que dice qué se compila) que compila el paquete entero se llama Torneo-Package.

5 · Si algo sale distinto. Si no sale ningún error, no guardaste el fichero o editaste otro Package.swift: debe ser el de ~/Marcador-Swift/Torneo. Si sale error: ... Invalid manifest, quedó una coma de más o de menos al borrar (el mensaje acaba con el fichero, la línea y el motivo).

6 · Comprueba. Lo que debes ver es lo contrario de lo habitual: el compilador falla, con errores de concurrencia (is not concurrency-safe, does not conform to the 'Sendable' protocol). Ese fallo es la prueba de que el ajuste está aplicado.

Paso · Marca Sendable y arregla Equipo

Objetivo: que el módulo Torneo compile en Swift 6. Al terminar, xcrun swift build (sin --build-tests) dice Build complete!.

Torneo
Sources
Torneo
Equipo.swiftse modifica
(otros ocho .swift)se modifican: solo `Sendable`
Nueve ficheros de `Sources` que cambian (ocho solo ganan `Sendable`), en el Project navigator.

Los ocho ficheros que solo ganan Sendable son Closures.swift, EventoDePartido.swift, Jugador.swift, Partido.swift, PartidoEnVivo.swift, Reglamento.swift, Simulacion.swift y Tabla.swift; cada uno lleva de una a tres líneas.

Trabajas en ~/Marcador-Swift/Torneo, y los comandos se escriben en la Terminal, dentro de esa carpeta.

La cura de la mayoría de los errores es la misma: marcar como Sendable lo que lo es (structs y enums) y las closures que cruzan tareas como @Sendable. El protocolo Reglamento también: un tipo que adopta un protocolo Sendable tiene que ser Sendable, y así un any Reglamento («cualquier tipo que cumple el protocolo») puede viajar entre tareas sin romper la garantía. Cuando Partido pasa a ser Sendable, sale el error de fondo, que lleva de nuevo al mismo sitio:

Sources/Torneo/Partido.swift:4:16: error: stored property 'local' of 'Sendable'-conforming struct 'Partido' has non-Sendable type 'Equipo'
Sources/Torneo/EventoDePartido.swift:24:10: error: associated value 'gol(minuto:jugador:equipo:)' of 'Sendable'-conforming enum 'EventoDePartido' contains non-Sendable type 'Equipo'

Equipo es la clase con el capitan que cambia, y la tabla de arriba dice qué hacer: un Mutex.

Intenta escribirlo tú: marca los tipos Sendable de uno en uno, compilando con xcrun swift build cuando quieras ver cómo cambia la lista de errores; al final, el Mutex de Equipo. Lo que debes ver al terminar: Build complete!.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código

Las marcas Sendable, de una en una. Cada bloque es la línea de declaración que ya tienes, sustituida por esta (solo cambia el final, Sendable, y la { que cierra la línea): no añadas una línea nueva. Jugador.swift:

Torneo/Sources/Torneo/Jugador.swift
public struct Jugador: Equatable, Hashable, Sendable {

Partido.swift:

Torneo/Sources/Torneo/Partido.swift
public struct Partido: Equatable, Sendable {

EventoDePartido.swift (tres tipos):

Torneo/Sources/Torneo/EventoDePartido.swift
public enum ColorDeTarjeta: String, CaseIterable, Codable, Sendable {
public enum EventoDePartido: Equatable, Sendable {
public enum Resultado: Equatable, Sendable {

Tabla.swift:

Torneo/Sources/Torneo/Tabla.swift
public struct FilaDePosicion: Equatable, Sendable {

Simulacion.swift:

Torneo/Sources/Torneo/Simulacion.swift
public struct Estadisticas: Equatable, Sendable {

Reglamento.swift:

Torneo/Sources/Torneo/Reglamento.swift
public protocol Reglamento: Sendable {

Closures.swift (las dos closures globales):

Torneo/Sources/Torneo/Closures.swift
public let esGol: @Sendable (EventoDePartido) -> Bool = {
public let esDelMinutoOAntes: @Sendable (EventoDePartido, Int) -> Bool = { evento, limite in

PartidoEnVivo.swift, dos cambios en el mismo tipo. La propiedad:

Torneo/Sources/Torneo/PartidoEnVivo.swift
    private let alTerminarLaEmision: (@Sendable () -> Void)?

y el parámetro del init, en el mismo sitio donde estaba:

Torneo/Sources/Torneo/PartidoEnVivo.swift
        alTerminarLaEmision: (@Sendable () -> Void)? = nil,

Equipo.swift, con cuatro cambios. Uno, el import va arriba del todo:

Torneo/Sources/Torneo/Equipo.swift
import Synchronization

Dos, la clase pasa a Sendable:

Torneo/Sources/Torneo/Equipo.swift
public final class Equipo: Sendable {

Tres, el public private(set) var capitan: Jugador? (debajo del init?) se sustituye por el cerrojo y una propiedad calculada. El comentario que tiene encima, sobre private(set), ya no describe nada: bórralo o cámbialo por «el capitán vive en un cerrojo»:

Torneo/Sources/Torneo/Equipo.swift
    private let capitanGuardado = Mutex<Jugador?>(nil)
 
    public var capitan: Jugador? { capitanGuardado.withLock { $0 } }

Cuatro, en nombrarCapitan, la asignación capitan = jugador pasa a:

Torneo/Sources/Torneo/Equipo.swift
        capitanGuardado.withLock { $0 = jugador }

Compila con xcrun swift build. Debe terminar con Build complete!. Si falta algún Sendable, el error nombra el tipo y el fichero: añade la marca ahí. Si sale stored property ... has non-Sendable type 'Equipo', es que Equipo todavía no tiene el Mutex.

Algo que no sale: main.swift llama a métodos async (await enDirecto.jugar()) sobre un PartidoEnVivo, que no es Sendable, y compila sin avisos. Lo creaste ahí mismo y nadie más lo usa, y Swift lo sabe: es la regla de sending de @MainActor y el hilo principal, que deja entregar un objeto no Sendable si nadie más lo toca.

Para ver cómo avisa el compilador de un tipo que falta, provócalo: quita Sendable de la línea de Jugador en Jugador.swift, compila (verás errores como non-'Sendable' type 'Jugador') y devuélvelo a su sitio.

Paso · Arregla los tests

Objetivo: que los tests también compilen en Swift 6. Al terminar, xcrun swift build --build-tests termina sin ningún warning: ni error:.

Ahora sí, compila los tests con xcrun swift build --build-tests. Salen tres errores nuevos, uno en ActoresTests.swift y dos en EventosEnVivoTests.swift, uno por cada test que usa terminado (los números de línea dependen de tus comentarios):

Tests/TorneoTests/ActoresTests.swift:25:23: error: passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure
Tests/TorneoTests/EventosEnVivoTests.swift:45:107: error: mutation of captured var 'terminado' in concurrently-executing code [#SendableClosureCaptures]
Tests/TorneoTests/EventosEnVivoTests.swift:59:108: error: mutation of captured var 'terminado' in concurrently-executing code [#SendableClosureCaptures]

El primero es el test de la clase compartida de Actores: MarcadorInseguro no es Sendable, y el compilador la rechaza en cuanto la mandas a un TaskGroup. Los otros dos son una var terminado = false que una closure @Sendable quiere cambiar desde otra tarea: no puede, porque dos tareas podrían tocarla a la vez.

Los arreglos:

  • En ActoresTests.swift, marcar MarcadorInseguro como @unchecked Sendable. Es la línea que hace callar al compilador, y el bug sigue ahí: se conserva a propósito, para que la prueba siga enseñando el fallo.
  • En EventosEnVivoTests.swift, sustituir las dos var terminado = false por una clase Bandera, Sendable, cuyo Bool vive en un Mutex (con subir() y estaLevantada).

Bandera es la taquilla del ejemplo de arriba con un Bool en vez de un Int: Mutex(false) deduce el tipo del valor inicial.

Intenta escribirlo tú:

  1. En ActoresTests.swift, MarcadorInseguro pasa a prometer, sin comprobación, que se puede compartir.
  2. En EventosEnVivoTests.swift, una clase privada Bandera, que se puede compartir entre tareas, con un valor que empieza bajado y vive en un cerrojo; un método subir, que lo levanta, y una propiedad estaLevantada, que lo lee.
  3. En los dos tests que usaban terminado, esa variable pasa a ser una bandera: se crea, se sube al terminar la emisión, y la espera y la comprobación preguntan si está levantada.

Lo que debes ver al compilar los tests: ningún error:.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código

En ActoresTests.swift, la clase de Actores pasa a @unchecked Sendable (sustituye esa línea de declaración):

Torneo/Tests/TorneoTests/ActoresTests.swift
private final class MarcadorInseguro: @unchecked Sendable {

En EventosEnVivoTests.swift, añade import Synchronization encima de los dos import que ya tienes (Testing y @testable import Torneo) y la clase Bandera debajo de ellos:

Torneo/Tests/TorneoTests/EventosEnVivoTests.swift
import Synchronization
import Testing
@testable import Torneo
 
private final class Bandera: Sendable {
    private let valor = Mutex(false)
    func subir() { valor.withLock { $0 = true } }
    var estaLevantada: Bool { valor.withLock { $0 } }
}

En los dos tests de EventosEnVivoTests.swift que usaban terminado (primerGolCancelaElResto y cancelarAlConsumidorCancelaAlProductor), cambian solo estas líneas; el msPorMinuto de cada test se queda como estaba:

cambios en cada test: antes y después
// antes:   var terminado = false
// después: let terminado = Bandera()
 
// antes:   alTerminarLaEmision: { terminado = true }
// después: alTerminarLaEmision: { terminado.subir() }
 
// antes:   for _ in 0..<50 where !terminado { … }
// después: for _ in 0..<50 where !terminado.estaLevantada { … }
 
// antes:   #expect(terminado)
// después: #expect(terminado.estaLevantada)

Vuelve a compilar con xcrun swift build --build-tests: sin errores ni avisos.

Lo que hace cada cambio de la lección:

CambioPara qué
Quitar swiftLanguageModes: [.v5]Vuelve el modo Swift 6 y sus errores
Sendable en structs, enums y ReglamentoQue crucen tareas
@Sendable en las closures globales y en alTerminarLaEmisionUna closure que cruza tareas no puede capturar estado mutable
Mutex en EquipoEl capitán, lo único que cambia, queda protegido: Equipo es Sendable sin promesas
@unchecked Sendable en MarcadorInseguroSolo en el test, para conservar el bug a la vista
Bandera con MutexUna closure @Sendable no puede mutar un var capturado

Kotlin y Swift, lado a lado

KotlinSwift
Pasar datos entre hilosSin comprobación: val, Mutex o StateFlow por disciplinaSendable, comprobado
CerrojoNo lo viste en este cursoMutex de Synchronization: bloquea el hilo un instante
Un var capturado por una tareaCompila; el riesgo es tuyoEn una closure @Sendable, error
Promesa de «esto es seguro»No existe@unchecked Sendable (sin comprobar)

Punto de control

  • Desde ~/Marcador-Swift/Torneo, xcrun swift build --build-tests termina sin ninguna línea con warning: ni error: (prueba con rm -rf .build antes si quieres compilarlo todo de cero: .build es la carpeta donde Swift deja lo compilado). El mismo resultado sale en Xcode con el esquema Torneo-Package y Product → Test (Cmd+U), si esa opción te aparece; si no, basta con los comandos de la Terminal.
  • xcrun swift run torneo-cli termina con estas dos líneas: Marcador tras 1000 goles de cada equipo a la vez: 1000-1000 y Marcador final del partido en vivo: 2-1.
  • xcrun swift test termina con Test run with 125 tests in 16 suites passed. El test de la clase compartida sigue perdiendo goles, ahora con @unchecked Sendable.
  • Provocaste un fallo: quitaste Sendable de Jugador (la línea de Jugador.swift), compilaste, viste el error que nombra Jugador y lo devolviste a su sitio.
  • Sabes decir por qué Equipo usa un Mutex y no @unchecked Sendable.
  • Tu Package.swift ya no tiene swiftLanguageModes, y sabes decir por qué sin esa línea el paquete se compila en modo Swift 6.

Tienes el paquete en Swift 6 y sin avisos. En la lección siguiente, El estado de pantalla en el actor principal, escribes con él la clase que guardará lo que una pantalla muestra.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Cuál de estos tipos puede ser Sendable sin promesas ni cerrojos?

  2. 2.¿Por qué Equipo guarda el capitán en un Mutex en lugar de marcar la clase @unchecked Sendable?

  3. 3.¿Qué hacía swiftLanguageModes: [.v5] en el Package.swift durante las lecciones anteriores?

  4. 4.¿Por qué una closure que cruza tareas se marca @Sendable y no puede capturar un var que cambia?

  5. 5.En capitanGuardado.withLock { $0 = jugador }, ¿qué hace el Mutex?

Faltan 5 de 5