Posts

Swift 6.4: limpieza asíncrona, menos copias y mejores herramientas

Arturo Rivas Arias

Swift 6.4 ya está disponible. La nueva versión combina cambios que simplifican funciones cotidianas con herramientas para controlar mejor la memoria y seguir llevando Swift a otras plataformas. Entre tantas novedades, merece la pena detenerse en las que resuelven problemas concretos: terminar una operación asíncrona correctamente, gestionar avisos durante una migración o almacenar recursos que no se pueden copiar. El anuncio oficial recoge el lanzamiento.

Una de las incorporaciones más prácticas es poder utilizar await dentro de defer. Hasta ahora, una limpieza de recursos asíncrona obligaba a repetir llamadas en varias rutas de salida o lanzar una tarea independiente. Con Swift 6.4, un contexto asíncrono puede esperar ese trabajo al abandonar el ámbito, tanto si termina normalmente como si propaga un error. Los bloques siguen ejecutándose en orden inverso al de su declaración. Así lo define SE-0493.

Imagina una sincronización de inventario que abre una sesión y debe finalizarla después de actualizar las existencias. Este protocolo representa el servicio; su implementación se encargaría de la comunicación:

protocol StockSession: Sendable {
    func begin() async throws
    func synchronize() async throws
    func end() async
}

func synchronizeStock(using session: some StockSession) async throws {
    try await session.begin()

    defer {
        await session.end()
    }

    try await session.synchronize()
}

La función espera a que end() termine antes de devolver el control. Si begin() falla, todavía no se ha alcanzado el defer, así que este no se ejecuta. Registrar la limpieza justo después de adquirir el recurso mantiene clara esa relación.

Hay un detalle fundamental: un defer asíncrono sigue observando la cancelación de su tarea. Si la implementación de end() consulta ese estado y abandona el trabajo, haberla colocado en defer no soluciona el problema. Para esos casos llega withTaskCancellationShield, definido en SE-0504. Podemos sustituir el bloque anterior por este:

defer {
    await withTaskCancellationShield {
        await session.end()
    }
}

Dentro de la protección, las comprobaciones de cancelación se comportan como si la tarea no estuviera cancelada. Al salir vuelve a observarse su estado real. La cancelación no se deshace y tampoco desaparecen los errores de red u otros fallos. Conviene reservar esta protección para trabajo breve de finalización o recuperación: protege frente a la cancelación de la tarea, pero no garantiza que un servidor responda o que el proceso siga vivo.

Otra mejora útil durante una migración es @diagnose, que permite ajustar un grupo de avisos dentro de una declaración. Admite tratarlos como errores, mantenerlos como avisos o ignorarlos, y permite documentar el motivo. Por ejemplo, podemos concentrar una compatibilidad temporal en una función concreta sin silenciar las advertencias del resto del proyecto. La sintaxis y sus límites están descritos en SE-0522.

@available(*, deprecated, message: "Usa el nuevo formato de etiquetas")
func legacyShelfLabel(_ code: String) -> String {
    "EST-\(code)"
}

@diagnose(
    DeprecatedDeclaration,
    as: ignored,
    reason: "Compatibilidad temporal con las etiquetas ya impresas"
)
func labelForExistingShelf(_ code: String) -> String {
    legacyShelfLabel(code)
}

El atributo actúa sobre advertencias del grupo indicado; no permite ocultar errores de compilación. Su utilidad está en delimitar una excepción y dejarla documentada junto al código que la necesita.

También desaparecen pequeñas fricciones de sintaxis. Un existencial opcional puede escribirse como any ShelfLabel?, sin los paréntesis de (any ShelfLabel)?. Además, anyAppleOS permite agrupar condiciones de disponibilidad cuando las versiones coinciden entre plataformas Apple, y añadir excepciones específicas cuando sea necesario. Apple explica estos cambios en la sesión What’s new in Swift.

En rendimiento, una pieza destacada es la llegada de UniqueArray y RigidArray, disponibles mediante import Containers. Ambos pueden almacenar elementos no copiables y poseen su almacenamiento de forma exclusiva. UniqueArray crece automáticamente; RigidArray trabaja con una capacidad que no aumenta de forma implícita al insertar. Si se llena, hay que gestionar esa situación o ampliar expresamente su almacenamiento. Los detalles están en SE-0527.

Esto cambia el significado de una asignación: trasladar un UniqueArray a otra variable consume el valor original, en lugar de crear una copia independiente. Así se evita el coste de comprobar y copiar almacenamiento compartido al modificarlo. Aun así, crecer puede seguir requiriendo una nueva reserva de memoria. Estas estructuras resultan interesantes para recursos no copiables o código cuyo coste debe ser más predecible; elegirlas implica aceptar unas reglas de propiedad más estrictas.

Ese trabajo se completa con Iterable, un protocolo que permite recorrer elementos prestados mediante la sintaxis habitual for-in. El problema que resuelve va más allá de acelerar un bucle: Sequence se diseñó alrededor de un iterador que devuelve valores, mientras que tipos como Span y los elementos no copiables necesitan expresar acceso temporal sin transferir necesariamente su propiedad. SE-0516 introduce esa base de iteración, que también admite errores durante el recorrido.

El préstamo tiene límites: un elemento cuyo acceso depende del iterador no puede conservarse libremente fuera del bucle. El compilador mantiene esa relación de duración para evitar que el acceso sobreviva al almacenamiento que lo proporciona. Es una ampliación del modelo de colecciones que permite escribir algoritmos genéricos para más tipos de datos.

Swift Testing y XCTest mejoran su interoperabilidad, lo que facilita reutilizar funciones auxiliares durante una migración. Se pueden registrar aserciones de XCTest desde pruebas de Swift Testing y usar #expect dentro de XCTest. Conviene revisar el modo configurado: en el modo limitado, determinados fallos de XCTest invocados desde Swift Testing se notifican como advertencias; el modo completo los registra como fallos. ST-0021 concreta esas diferencias. Migrar las pruebas exige comprobar también cómo repercuten sus resultados en la integración continua.

Las herramientas acompañan estos cambios: SwiftPM utiliza Swift Build por defecto, y el seguimiento preciso de dependencias de módulos mejora la información que utiliza LLDB. Subprocess alcanza ya la versión 1.0. En otras plataformas destacan la conversión entre Span y std::span de C++20, nuevas capacidades de interoperabilidad con Java y mejoras de Embedded Swift. JavaScriptKit anuncia una comunicación segura con WebAssembly hasta 40 veces más rápida que la conversión dinámica anterior; la cifra corresponde a esa operación específica. Si necesitas conocer más detalles puedes hacerlo en la publicación del lanzamiento de Swift 6.4.

Para una aplicación existente, una adopción razonable empieza por localizar cierres asíncronos de recursos que estén duplicados, revisar las excepciones de compilación y comprobar las pruebas durante su migración. Las nuevas colecciones merecen una evaluación allí donde la propiedad de los recursos o las mediciones justifiquen el cambio. Swift 6.4 ofrece más precisión para expresar esas decisiones directamente en el código, pero son una optimización que no todas las aplicaciones necesitan.