Usa un grupo de tareas de Swift para recopilar cálculos concurrentes en orden de finalización, omitir nil y cancelar las tareas secundarias restantes cuando llegue un valor. Este ejemplo de línea de comandos para Mac encuentra una disposición rectangular válida para mosaicos sintéticos y explica cuándo puede devolverse realmente el resultado.

Define qué se considera el primer resultado útil

Supongamos que una pequeña herramienta de diseño para escritorio necesita organizar 221 mosaicos sintéticos en un rectángulo. Varios cálculos locales buscan distintas cantidades posibles de filas. Se acepta cualquier factor no trivial: 13 filas y 17 columnas funciona, al igual que 17 filas y 13 columnas. La tarea del lector es aceptar el primer cálculo válido que termine.

Este es un contrato basado en el orden de finalización. No selecciona el factor más pequeño, el rectángulo más equilibrado ni el cálculo que aparece primero en la lista. Apple documenta que los resultados sucesivos de un grupo de tareas llegan en orden de finalización, independientemente del orden de inserción. TaskGroup.next(isolation:)

Define el comportamiento antes de escribir el asistente:

Situación Comportamiento requerido
Un cálculo termina con nil Sigue esperando a los demás cálculos.
Un cálculo termina con un valor Selecciónalo y solicita la cancelación del trabajo restante.
Todos los cálculos devuelven nil Devuelve nil.
La lista de cálculos está vacía Devuelve nil.
Se detecta la cancelación del padre Deja de seleccionar resultados y devuelve nil.

La última fila es una decisión deliberada sobre la API para este ejemplo. Aquí, nil significa que la invocación no proporciona ningún valor utilizable; no distingue entre una búsqueda infructuosa y una cancelación. Si el código que llama al asistente necesita esa distinción, usa un tipo de resultado explícito o un contrato que pueda lanzar errores.

Crea un ejemplo local completo en tu Mac

Usa una cadena de herramientas Swift 6 o posterior. En Terminal, compruébalo con swift --version. Si Swift no está disponible, sigue las instrucciones oficiales de Install Swift — macOS. El programa usa Foundation y las API de concurrencia de Swift, sin dependencias de paquetes externos.

Crea un nuevo paquete ejecutable con estos comandos. Este procedimiento sigue el flujo documentado por Swift.org para crear y ejecutar paquetes. Build a Command-line Tool

mkdir FirstNonNilDemo
cd FirstNonNilDemo
swift package init --name FirstNonNilDemo --type executable

Reemplaza el código fuente ejecutable generado por el programa completo que aparece a continuación. Según la estructura generada, puede llamarse Sources/FirstNonNilDemo/FirstNonNilDemo.swift o Sources/main.swift. Usa un único archivo de punto de entrada llamado FirstNonNilDemo.swift; si la plantilla creó main.swift, cambia el nombre de ese archivo antes de añadir el programa @main. La guía de Swift documenta que un programa usa main.swift o un punto de entrada @main. Build a Command-line Tool

import Foundation

func firstNonNil<Value: Sendable>(
    _ calculations: [@Sendable () async -> Value?]
) async -> Value? {
    guard !Task.isCancelled else { return nil }

    return await withTaskGroup(
        of: Value?.self,
        returning: Value?.self
    ) { group in
        defer { group.cancelAll() }

        for calculation in calculations {
            let added = group.addTaskUnlessCancelled {
                guard !Task.isCancelled else { return nil }
                return await calculation()
            }
            guard added else { break }
        }

        for await candidate in group {
            guard !Task.isCancelled else { return nil }
            if let value = candidate {
                return value
            }
        }

        return nil
    }
}

func findFactor(total: Int, range: ClosedRange<Int>) -> Int? {
    guard total > 1 else { return nil }

    for rows in range {
        guard !Task.isCancelled else { return nil }
        guard rows > 1, rows < total else { continue }

        if total % rows == 0 {
            return rows
        }
    }

    return nil
}

@main
struct FirstNonNilDemo {
    static func main() async {
        let checks: [@Sendable () async -> Int?] = [
            { nil },
            { 42 }
        ]
        let checkResult = await firstNonNil(checks)
        precondition(checkResult == 42)

        let empty: [@Sendable () async -> Int?] = []
        let emptyResult = await firstNonNil(empty)
        precondition(emptyResult == nil)

        for rawText in [" 221\n", " 223 ", "tiles"] {
            let text = rawText.trimmingCharacters(
                in: .whitespacesAndNewlines
            )

            guard let total = Int(text), total > 1 else {
                print("Input must be a whole number greater than 1.")
                continue
            }

            let calculations: [@Sendable () async -> Int?] = [
                { findFactor(total: total, range: 2...5) },
                { findFactor(total: total, range: 6...16) },
                { findFactor(total: total, range: 17...30) }
            ]

            if let rows = await firstNonNil(calculations) {
                print("\(total) tiles: \(rows) rows x \(total / rows) columns")
            } else {
                print("\(total) tiles: no factor in the supplied ranges.")
            }
        }
    }
}

Ejecuta el paquete desde su directorio:

swift run FirstNonNilDemo

Para 221, se acepta tanto 221 tiles: 13 rows x 17 columns como 221 tiles: 17 rows x 13 columns. Para 223, el programa informa que no hay ningún factor dentro de los rangos proporcionados. El texto tiles genera el mensaje de entrada. Estos son resultados esperados, no una transcripción de una ejecución registrada. La aritmética se comprobó por separado; no había un compilador de Swift disponible en el entorno de investigación, así que se inspeccionó el programa Swift, pero no se ejecutó.

Foundation solo se ocupa de limpiar los espacios en blanco circundantes. Su método trimmingCharacters(in:) elimina los caracteres coincidentes de ambos extremos; no elimina caracteres del interior de la cadena. Por tanto, 221 se convierte en 221, mientras que 2 21 sigue siendo una entrada no válida para este programa. trimmingCharacters(in:)

Por qué el asistente omite los valores nulos sin perder resultados posteriores

El tipo del resultado secundario del grupo es Value?. Por tanto, cada tarea secundaria proporciona un valor utilizable o un resultado vacío normal. El bucle for await recibe los resultados de las tareas secundarias que terminan, y su enlace opcional interno selecciona únicamente un candidato distinto de nil.

Si usas directamente group.next(), hay dos tipos distintos de ausencia. El tipo de retorno documentado es ChildTaskResult?. Con resultados secundarios opcionales, esto se convierte en Value??: el opcional externo representa si existe otro resultado de una tarea secundaria, y el opcional interno representa si esa tarea encontró un valor. TaskGroup.next(isolation:)

Esta distinción explica un error común: tomar un resultado y devolverlo de inmediato. Si el primer cálculo que termina devuelve nil, puede que otras tareas secundarias aún tengan respuestas útiles. El bucle del ejemplo completo sigue consumiendo resultados hasta encontrar un valor o hasta que se agote el grupo.

El asistente usa addTaskUnlessCancelled y comprueba el valor booleano que devuelve. Apple documenta que este método no crea una tarea secundaria si el grupo ya está cancelado. Salir del bucle de envío evita seguir enviando trabajo una vez detectada esa condición. addTaskUnlessCancelled(priority:operation:)

Los cálculos capturan el recuento inmutable de mosaicos y devuelven sus respuestas a través del grupo. No actualizan una variable compartida que guarde el resultado ganador. El resultado genérico debe ajustarse a Sendable, de acuerdo con la restricción documentada para grupos de tareas. withTaskGroup(of:returning:isolation:body:)

Por qué seleccionar un ganador no significa devolverlo de inmediato

Devolver un valor desde el cierre del grupo selecciona la respuesta, pero el withTaskGroup que lo contiene sigue esperando a que terminen todas las tareas secundarias. Apple documenta explícitamente que incluso las tareas secundarias canceladas deben completarse antes de que la función devuelva el resultado. withTaskGroup(of:returning:isolation:body:)

El bloque defer llama a group.cancelAll() cuando el cierre termina. Esto solicita la cancelación de los cálculos restantes. La cancelación no interrumpe los cálculos de forma arbitraria: las tareas secundarias deben cooperar y terminar antes por sí mismas. TaskGroup

En este ejemplo, findFactor comprueba Task.isCancelled antes de cada divisor candidato. Cada unidad de trabajo es pequeña, por lo que hay oportunidades frecuentes para detenerse. Para un cálculo mayor, coloca comprobaciones entre bloques acotados y significativos. Una comprobación solo antes de una operación larga e indivisible deja que esa operación siga ejecutándose hasta que termine. La guía del lenguaje Swift describe la cancelación como cooperativa e indica que devolver nil es una de las respuestas admitidas. Concurrency

Si un cálculo concurrente nunca termina ni responde a la cancelación, este asistente estructurado no puede garantizar un retorno rápido. La solución admitida consiste en hacer que cada tarea de trabajo tenga una duración acotada o responda a la cancelación. Añadir un resultado de tiempo de espera concurrente no elimina la obligación del grupo de esperar a sus tareas secundarias.

La cancelación del padre se comprueba antes del envío, al inicio de cada tarea secundaria, dentro del bucle aritmético y antes de aceptar cada resultado. Estas comprobaciones detectan la cancelación en momentos concretos; no crean una regla atómica que garantice que la cancelación siempre prevalezca sobre un valor. Si la cancelación llega después de la última comprobación, puede coincidir con un retorno satisfactorio.

Elige la regla de selección adecuada y verifica sus límites

El ejemplo sintético ofrece una prueba útil para tomar una decisión: tanto 13 como 17 satisfacen el contrato establecido. Si tu herramienta requiere la cantidad menor de filas, aceptar cualquiera de los dos resultados es incorrecto. El orden de finalización solo sirve cuando todas las respuestas aceptadas son intercambiables para la tarea del lector.

Qué necesita el código que llama Enfoque adecuado
Cualquier valor aceptable a medida que terminan los cálculos Usa el bucle del grupo de tareas mostrado arriba.
El primer cálculo exitoso según una lista de preferencias fija Evalúa en orden de preferencia o conserva los índices y resuelve los candidatos anteriores antes de elegir.
El resultado más pequeño o con la mejor puntuación Compara los resultados pertinentes antes de seleccionar.
Varios resultados conocidos que se necesitan juntos Considera async let.

La guía del lenguaje Swift presenta async let para tareas secundarias conocidas cuyos resultados se necesitan más adelante, y los grupos de tareas para controlar explícitamente las tareas secundarias que se añaden de forma dinámica. Esperar a los resultados con nombre en un orden elegido no implementa un bucle de selección por orden de finalización. Concurrency

Usa estas comprobaciones al adaptar el programa:

  • Conserva la aserción para la lista vacía: debe devolver nil.
  • Conserva la aserción mixta con nil y 42: debe devolver 42, independientemente del orden de finalización.
  • Prueba cálculos que devuelvan todos nil.
  • Si hay varias respuestas válidas, verifica que el resultado pertenezca al conjunto válido en vez de exigir un ganador concreto.
  • Inspecciona cada tarea de trabajo para asegurarte de que el progreso sea acotado y haya comprobaciones de cancelación.
  • Indica que la búsqueda no tuvo éxito dentro de los rangos proporcionados; no infieras que no existe ninguna respuesta en otros rangos.

Los tres pequeños recorridos de factores demuestran el comportamiento, no una mejora de velocidad. Para estos números ilustrativos basta con un recorrido secuencial. Usa el patrón de cálculos concurrentes cuando la tarea real se beneficie de aceptar resultados intercambiables, y mide esa carga de trabajo antes de decidir cuánto trabajo ejecutar de forma concurrente.

Define explícitamente la regla de aceptación, omite los resultados vacíos de las tareas secundarias y haz que el trabajo restante pueda detenerse. Así, la herramienta para Mac ofrece un contrato de valor predecible: el primer resultado utilizable que entregue el grupo, seguido de la finalización del cierre cooperativo de las tareas secundarias.

Fuentes