Para conservar los errores tipados en cálculos concurrentes de Swift, devuelve un Result tipado desde cada tarea hija y desenvuélvelo después de recopilar los resultados. Este ejemplo para Mac calcula cantidades sintéticas de píxeles de rectángulos y conserva el orden de entrada, los errores explícitos y un contrato de lote en el que todo tiene éxito o todo falla.
¿Por qué usar resultados tipados en el límite del grupo de tareas?
Supongamos que estás creando una vista previa de gráficos para escritorio que calcula la cantidad de píxeles de varios rectángulos. Cada cálculo puede rechazar dimensiones no válidas o informar de un desbordamiento de enteros. El código que llama necesita una matriz completa y ordenada o un error de cálculo específico para poder gestionar explícitamente todos los fallos admitidos.
Swift incorporó los errores tipados en Swift 6.0. Una declaración como throws(CalculationError) limita los valores que puede lanzar la función a ese tipo de error. Esto resulta útil para un contrato de cálculo pequeño y cerrado cuyas implementaciones y código de llamada evolucionan juntos. Consulta SE-0413: Typed throws.
El límite de la API de concurrencia es importante. La firma documentada por Apple de withThrowingTaskGroup proporciona un ThrowingTaskGroup<ChildTaskResult, any Error> al cuerpo. El tipo de grupo, aunque parezca genérico, no ofrece un parámetro concreto de error de dominio a través de esa factoría. Por tanto, añadir errores tipados solo a tu cálculo no establece un tipo concreto de error en toda esta API de grupo. Consulta withThrowingTaskGroup.
Una opción compatible es usar withTaskGroup y hacer que cada tarea hija devuelva Result<Int, CalculationError>. La tarea termina normalmente con un valor que representa el éxito o el fallo. Después de recopilar esos valores, la función de lote llama a get(), cuya firma documentada es throws(Failure). Así se restablecen los errores tipados sin una conversión en tiempo de ejecución. Consulta la documentación de Result y la documentación de get() de Apple.
Define el contrato del lote antes de escribir el código concurrente
En este ejemplo, cada entrada contiene un ancho y un alto enteros, medidos en píxeles. El cálculo consiste en multiplicarlos. Todo es sintético y se mantiene en memoria; el programa no renderiza una imagen ni carga o guarda datos de la aplicación.
Las siguientes políticas son decisiones de diseño para este ejemplo. Hacen que tanto los valores devueltos como el error seleccionado sean previsibles:
| Situación | Comportamiento del lote |
|---|---|
| Todos los rectángulos son válidos | Devuelve las cantidades de píxeles en el orden de entrada |
| El ancho o el alto es cero o negativo | Indica que ese rectángulo no es válido |
El producto no cabe en Int |
Informa de un desbordamiento para ese rectángulo |
| Fallan varios rectángulos | Informa del fallo con el índice de entrada más bajo |
| Se detecta la cancelación en un punto de control del lote | Informa de .cancelled |
| La matriz de entrada está vacía | Devuelve una matriz vacía, salvo que se detecte la cancelación |
Seleccionar el índice de entrada más bajo requiere recopilar el resultado de cada tarea hija antes de elegir un error de cálculo. Swift no garantiza el orden de planificación del grupo de tareas, y el orden de finalización puede diferir del orden de envío. Asociar un índice a cada resultado permite al recopilador restaurar el orden previsto. Consulta TaskGroup.
Esta política es adecuada para un lote pequeño de vistas previas en el que importa que los resultados sean repetibles. Espera deliberadamente a que terminen todos los cálculos. Un diseño que informe del primer fallo observado durante la ejecución haría un intercambio distinto y podría seleccionar un rectángulo diferente en cada ejecución.
Crea un ejemplo local completo en tu Mac
Usa una cadena de herramientas de Swift 6 o posterior ya instalada. En Terminal, compruébala con swift --version. Swift.org documenta este comando en Getting Started y ofrece instrucciones de instalación para Mac en Install Swift — macOS.
Crea un paquete ejecutable pequeño siguiendo el procedimiento documentado en la página de instalación:
mkdir TypedCalculations
cd TypedCalculations
swift package init --name TypedCalculations --type executable
Sustituye el archivo de entrada de Swift generado en Sources por los siguientes tres bloques de código, en orden, dentro de un solo archivo. Deja un único punto de entrada ejecutable. Este ejemplo usa el aislamiento de actor predeterminado habitual del paquete; si un proyecto está configurado para usar el actor principal como aislamiento predeterminado, sus tipos y funciones de cálculo deben colocarse fuera de ese aislamiento.
Define las entradas y los errores concretos
La enumeración de errores conserva información estructurada: el caso identifica el problema y su valor asociado identifica la entrada. El protocolo LocalizedError de Foundation proporciona un errorDescription opcional para mostrarlo. Los mensajes en inglés que aparecen a continuación son ejemplos; adoptar el protocolo no los traduce automáticamente. Consulta LocalizedError.
import Foundation
struct Rectangle: Sendable {
let width: Int
let height: Int
}
enum CalculationError: Error, Sendable, LocalizedError {
case invalidDimensions(index: Int)
case overflow(index: Int)
case cancelled
var errorDescription: String? {
switch self {
case .invalidDimensions(let index):
return "Rectangle \(index + 1) needs positive dimensions."
case .overflow(let index):
return "Rectangle \(index + 1) has too many pixels for Int."
case .cancelled:
return "Calculation cancelled."
}
}
}
struct IndexedOutcome: Sendable {
let index: Int
let result: Result<Int, CalculationError>
}
Calcula, recopila y restablece los errores tipados
La operación aritmética usa multipliedReportingOverflow(by:). Apple documenta que su partialValue es el producto completo solo cuando overflow es falso; de lo contrario, contiene un producto truncado. La comprobación siguiente impide que ese valor truncado se considere una cantidad válida de píxeles. Consulta multipliedReportingOverflow(by:).
func pixelCount(
for rectangle: Rectangle,
index: Int
) throws(CalculationError) -> Int {
if Task.isCancelled {
throw .cancelled
}
guard rectangle.width > 0, rectangle.height > 0 else {
throw .invalidDimensions(index: index)
}
let product = rectangle.width.multipliedReportingOverflow(
by: rectangle.height
)
guard !product.overflow else {
throw .overflow(index: index)
}
return product.partialValue
}
func calculateBatch(
_ rectangles: [Rectangle]
) async throws(CalculationError) -> [Int] {
if Task.isCancelled {
throw .cancelled
}
let outcomes: [IndexedOutcome] = await withTaskGroup(
of: IndexedOutcome.self,
returning: [IndexedOutcome].self
) { group in
for (index, rectangle) in rectangles.enumerated() {
group.addTask {
let result = Result<Int, CalculationError> {
() throws(CalculationError) -> Int in
try pixelCount(for: rectangle, index: index)
}
return IndexedOutcome(index: index, result: result)
}
}
var collected: [IndexedOutcome] = []
for await outcome in group {
collected.append(outcome)
}
return collected.sorted { $0.index < $1.index }
}
if Task.isCancelled {
throw .cancelled
}
var counts: [Int] = []
counts.reserveCapacity(outcomes.count)
for outcome in outcomes {
counts.append(try outcome.result.get())
}
return counts
}
La anotación explícita throws(CalculationError) en el cierre de Result hace visible su contrato de errores en lugar de depender de la inferencia del cierre. SE-0413 señala que los cambios de inferencia de errores tipados completos previstos originalmente no se implementaron en Swift 6.0. Consulta la nota de implementación en Typed throws.
Solo el recopilador padre añade elementos a collected. Cada tarea hija recibe un rectángulo y un índice, y devuelve su propio resultado Sendable. Esto cumple el requisito del grupo de tareas de que los valores devueltos por las tareas hijas sean Sendable; el cuerpo no lanzador del grupo los recopila con for await. Consulta withTaskGroup.
Añade un punto de entrada ejecutable
Este bloque final proporciona todas las entradas de ejemplo y llama a la función de lote. La asignación explícita dentro de catch también comprueba en tiempo de compilación el contrato previsto de la API: el valor capturado debe ser un CalculationError.
@main
struct TypedCalculations {
static func main() async {
let examples: [(name: String, rectangles: [Rectangle])] = [
(
name: "Valid",
rectangles: [
Rectangle(width: 640, height: 480),
Rectangle(width: 320, height: 200)
]
),
(
name: "Multiple failures",
rectangles: [
Rectangle(width: 0, height: 20),
Rectangle(width: Int.max, height: 2)
]
),
(
name: "Overflow",
rectangles: [
Rectangle(width: Int.max, height: 2)
]
),
(name: "Empty", rectangles: [])
]
for example in examples {
do {
let counts = try await calculateBatch(example.rectangles)
print("\(example.name): \(counts)")
} catch {
let failure: CalculationError = error
print("\(example.name): \(failure.errorDescription ?? "Calculation failed.")")
}
}
}
}
Ejecuta el programa desde el directorio del paquete:
swift run TypedCalculations
A continuación se muestra la salida esperada de la aplicación, sin incluir los mensajes de compilación. No había ningún compilador de Swift disponible en el entorno de investigación, por lo que el programa se revisó según la documentación de la API consultada, pero no se compiló ni se ejecutó allí.
Valid: [307200, 64000]
Multiple failures: Rectangle 1 needs positive dimensions.
Overflow: Rectangle 1 has too many pixels for Int.
Empty: []
Las cantidades válidas se deducen directamente de las entradas ilustrativas: 640 multiplicado por 480 da 307.200, y 320 multiplicado por 200 da 64.000. En el ejemplo con varios fallos, prevalece el primer rectángulo porque los resultados se examinan en el orden de entrada, aunque la segunda tarea hija termine antes.
¿Cómo se comportan la cancelación y los resultados parciales?
La cancelación en Swift es cooperativa. Una tarea debe comprobar si se ha cancelado y responder; solicitar la cancelación no interrumpe automáticamente su cálculo. Además, los grupos de tareas estructurados esperan a que terminen sus tareas hijas antes de devolver el control. Consulta el capítulo sobre concurrencia de la guía del lenguaje de Swift y la documentación de withTaskGroup de Apple.
Aquí se comprueba Task.isCancelled antes del envío, dentro de cada cálculo y después de la recopilación. Cuando un punto de control detecta la cancelación, el código la representa con el caso existente .cancelled. Leer el valor booleano evita introducir otro tipo de error en este contrato cerrado. Apple documenta esta propiedad en Task.isCancelled.
La comprobación posterior a la recopilación da prioridad a la cancelación del lote detectada frente a los errores de los rectángulos. Estas comprobaciones son puntos de control, no una garantía atómica: una solicitud de cancelación posterior a la última comprobación podría no afectar al resultado devuelto. Si un cálculo más largo contiene trabajo repetido, añade comprobaciones dentro de ese trabajo para detectar antes la cancelación.
En esta implementación, el fallo de una tarea hija no provoca su cancelación. Devuelve un valor normal Result.failure y el recopilador continúa. Si todas las tareas hijas terminan correctamente, quien realiza la llamada recibe todas las cantidades. Si al desenvolver los resultados se encuentra un fallo, la función lanza un error y la matriz counts, ensamblada parcialmente y de ámbito local, no se devuelve.
Si la interfaz de escritorio necesita en cambio un estado de éxito o fallo para cada rectángulo, cambia el contrato público del lote para que devuelva los resultados ordenados. Mantén cada fallo asociado a su índice. Ese comportamiento de cara a quien consulta el resultado difiere de la matriz en la que todo tiene éxito o todo falla implementada aquí.
Diagnostica la pérdida del tipo de error sin ocultar fallos
Si tu adaptación del código deja de compilar, sigue el tipo por cada límite en este orden:
- Comprueba que el cálculo declare
throws(CalculationError). - Comprueba que el valor de la tarea hija sea
Result<Int, CalculationError>, en lugar de un resultado cuyo tipo de fallo seaany Error. - Mantén la anotación explícita de errores tipados del cierre si la inferencia amplía su tipo de error.
- Usa
withTaskGroup, que no lanza errores, para transportar estos valoresResult. - Desenvuelve el resultado con
get()dentro de la función de lote tipada. - Comprueba si alguna operación lanzadora que hayas añadido introduce otro tipo de error.
Una conversión forzada no puede demostrar que se cumple este contrato. Del mismo modo, descartar los resultados fallidos o sustituirlos por cero cambiaría el significado del cálculo. Conserva los fallos como valores hasta que la política de lote elegida determine cómo comunicarlos.
Para estas multiplicaciones tan pequeñas, la concurrencia es una demostración arquitectónica; no se afirma que mejore el rendimiento. El resultado útil es un contrato preciso: las cantidades correctas conservan el orden de entrada, los fallos múltiples siguen una regla de selección determinista y quienes llaman reciben una enumeración concreta de errores que pueden gestionar directamente.
Fuentes
Typed throws — SE-0413 — Swift Project.
withThrowingTaskGroup(of:returning:isolation:body:) — Apple.
Result — Apple.
get() — Apple.
TaskGroup — Apple.
Getting Started — Swift.org.
Install Swift — macOS — Swift.org.
LocalizedError — Apple.
multipliedReportingOverflow(by:) — Apple.
Concurrency — The Swift Programming Language — Swift Project.
isCancelled — Apple.