Un cálculo Swift diferido mantiene vivo a su propietario cuando su tarea captura fuertemente a ese propietario. Usa una captura débil para permitir que el propietario desaparezca durante la espera, o captura una instantánea independiente de valores cuando el cálculo deba terminar sin él.

Para un desarrollador de Mac que crea una pequeña vista previa de un cálculo de escritorio, la tarea práctica consiste en elegir ese ciclo de vida deliberadamente. Este ejemplo espera brevemente, suma los valores sintéticos [4, 8, 12] y da formato al total. Todo se ejecuta localmente en memoria.

¿Qué ciclo de vida debería tener tu cálculo?

Las listas de captura de Swift permiten especificar qué retiene un cierre. Una captura fuerte mantiene la instancia referenciada; una referencia débil permite que se desasigne y pasa a ser nil después. Estos son comportamientos del lenguaje documentados, independientes de cuánto tiempo espere el cálculo. Conteo automático de referencias

Elige el contrato antes de elegir la sintaxis de captura. La siguiente tabla de decisión aplica esas reglas del lenguaje a una vista previa de escritorio diferida:

Comportamiento deseado Estrategia de captura ¿Qué ocurre después de que quien llama libera su propietario?
Terminar usando el mismo propietario [self] La tarea mantiene disponible al propietario para realizar su trabajo.
Continuar solo si el propietario sigue disponible [weak self] Después de esperar, la tarea comprueba si el propietario aún existe.
Terminar usando los valores seleccionados al programar la tarea [samples = self.samples] La tarea retiene los valores capturados sin retener a este propietario.

La fila de la instantánea tiene una condición esencial: este ejemplo captura una matriz de enteros. Las expresiones de la lista de captura se evalúan cuando se crea el cierre. Capturar una instancia de clase en su lugar retendría una referencia a esa instancia; no crearía una copia independiente de su contenido mutable. Expresiones

También hay una cuestión de valores aparte. Las versiones fuerte y débil de abajo leen los valores del propietario después de la demora. La versión de la instantánea usa los valores del momento en que se programó la tarea. Si las entradas de la vista previa pueden cambiar mientras se espera, decide qué interpretación debería representar el resultado.

Ejecuta una comparación local completa en tu Mac

Usa macOS 13 o posterior y una cadena de herramientas Swift 6 o posterior instalada. La sobrecarga de Task.sleep(for:) basada en duración que se usa aquí está documentada para macOS 13 en adelante. Suspende la tarea sin bloquear el hilo subyacente y produce CancellationError si la cancelación ocurre antes de que termine la espera. sleep(for:tolerance:clock:)

Guarda el siguiente programa completo como CaptureDemo.swift. La calculadora, los cierres de las tareas y la demostración se ejecutan en @MainActor, lo que mantiene serializado el acceso a su estado. Swift documenta tanto el aislamiento de actor principal a nivel de clase como la anotación explícita del cierre que se usa aquí. Concurrencia

import Foundation

enum CapturePolicy: String {
    case strong, weak, snapshot
}

@MainActor
func formatTotal(_ samples: [Int]) -> String {
    let total = samples.reduce(0, +)
    let formatter = NumberFormatter()
    formatter.numberStyle = .decimal
    formatter.locale = Locale(identifier: "en_US_POSIX")
    formatter.usesGroupingSeparator = false
    formatter.maximumFractionDigits = 0

    guard let text = formatter.string(from: NSNumber(value: total)) else {
        return "Formatting unavailable"
    }
    return "Total: \(text)"
}

@MainActor
final class Calculator {
    var samples = [4, 8, 12]

    func start(_ policy: CapturePolicy) -> Task<String, Error> {
        switch policy {
        case .strong:
            return Task { @MainActor [self] in
                try await Task.sleep(for: .milliseconds(300))
                try Task.checkCancellation()
                return formatTotal(self.samples)
            }

        case .weak:
            return Task { @MainActor [weak self] in
                try await Task.sleep(for: .milliseconds(300))
                try Task.checkCancellation()
                guard let self else {
                    return "Owner unavailable"
                }
                return formatTotal(self.samples)
            }

        case .snapshot:
            return Task { @MainActor [samples = self.samples] in
                try await Task.sleep(for: .milliseconds(300))
                try Task.checkCancellation()
                return formatTotal(samples)
            }
        }
    }
}

@main
struct CaptureDemo {
    @MainActor
    static func demonstrate(
        _ policy: CapturePolicy,
        cancel: Bool = false
    ) async {
        var owner: Calculator? = Calculator()
        weak var probe = owner
        let task = owner!.start(policy)
        owner = nil

        let label = cancel ? "\(policy.rawValue), cancelled" : policy.rawValue
        print("\(label) retains owner: \(probe != nil)")

        if cancel {
            task.cancel()
        }

        do {
            let result = try await task.value
            print(result)
        } catch is CancellationError {
            print("Cancelled")
        } catch {
            print("Calculation failed: \(error)")
        }
    }

    @MainActor
    static func main() async {
        await demonstrate(.strong)
        await demonstrate(.weak)
        await demonstrate(.snapshot)
        await demonstrate(.snapshot, cancel: true)
    }
}

La aritmética usa reduce(0, +) de la biblioteca estándar para combinar los enteros en una sola suma. NumberFormatter de Foundation convierte esa suma en texto para mostrar; su método string(from:) devuelve una cadena opcional, así que el programa informa explícitamente de un error de formato. La suma de la muestra fija da 24. reduce(::), NumberFormatter, string(from:)

En Terminal, cambia a la carpeta que contiene el archivo y ejecuta estos comandos. El destino especifica macOS 13 para la arquitectura del procesador de tu Mac. La opción -parse-as-library permite el punto de entrada @main explícito del archivo. El compilador de Swift acepta archivos de código fuente y opciones de línea de comandos; swiftc -help muestra las opciones instaladas con tu cadena de herramientas. Manual de línea de comandos de Swift

swift --version
swiftc -parse-as-library -swift-version 6 -target "$(uname -m)-apple-macos13.0" CaptureDemo.swift -o CaptureDemo
./CaptureDemo

A continuación se muestra el resultado esperado de la aplicación. Se deriva de las reglas de captura y cancelación; el entorno de investigación de este manuscrito no tenía un compilador Swift disponible, por lo que el programa se revisó, pero no se ejecutó aquí.

strong retains owner: true
Total: 24
weak retains owner: false
Owner unavailable
snapshot retains owner: false
Total: 24
snapshot, cancelled retains owner: false
Cancelled

La referencia débil probe permite observar la propiedad sin añadir otra referencia fuerte. Cada demostración elimina su propia referencia fuerte antes de esperar la tarea. Como la demostración y los cierres de las tareas usan el mismo actor, el cierre recién creado no puede intercalarse con esa configuración síncrona. Así, la comparación no depende de adivinar con qué rapidez se inicia la tarea. Concurrencia

¿Por qué importa la posición de la comprobación débil?

En la rama débil, guard let self aparece después de la demora. Así, la tarea puede esperar sin mantener al propietario de forma fuerte. Si el propietario ha desaparecido, el resultado explícito Owner unavailable termina ese cálculo.

Mover la desenvoltura antes de la demora cambia el ciclo de vida. El siguiente fragmento ilustrativo sustituye el cuerpo del cierre de la rama débil; no es un programa aparte:

// Inside the existing [weak self] task closure:
guard let self else {
    return "Owner unavailable"
}
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
return formatTotal(self.samples)

Aquí, la vinculación opcional exitosa crea una referencia local fuerte que se necesita de nuevo después de la suspensión. En consecuencia, el propietario sigue disponible durante la espera una vez que esa vinculación se completa. Esto se deriva de las reglas de referencias fuertes de Swift: la captura débil determina cómo accede inicialmente el cierre al propietario, mientras que la variable local desenvuelta determina cómo lo mantiene el código posterior. Conteo automático de referencias

Para el contrato «esperar sin retener al propietario y después calcular solo si sigue existiendo», mantén la desenvoltura después de la espera y realiza el pequeño cálculo justo después. Los puntos de suspensión adicionales después de desenvolver requieren otra decisión sobre el ciclo de vida.

Una captura unowned requiere que la instancia referenciada siga siendo válida cada vez que se acceda a ella. No proporciona la comprobación de ausencia opcional que necesita este ejemplo, donde se pretende que el propietario se libere durante la demora. Conteo automático de referencias

¿Liberar el identificador de la tarea detiene el cálculo?

Una tarea sigue ejecutándose aunque descartes su identificador. Conservarlo te permite esperar el resultado o solicitar la cancelación. Apple también documenta que una tarea completada libera de forma inmediata su cierre de operación y las referencias que este mantenía. Por tanto, una captura fuerte finita extiende el ciclo de vida del propietario durante el trabajo, sin necesariamente producir un ciclo de referencias duradero. Tarea

La cancelación es un contrato distinto del de propiedad. La demostración final solicita la cancelación mediante el identificador. La espera que lanza errores y el Task.checkCancellation() posterior ofrecen puntos en los que la operación puede detenerse, y quien llama informa de ese resultado. La cancelación de Swift es cooperativa: solicitarla no interrumpe por la fuerza código de cálculo arbitrario. Tarea

No sustituyas la espera que lanza errores por try? para luego continuar sin condiciones si la cancelación debe impedir un resultado. Suprimir ese error permitiría que la ejecución llegara al cálculo, a menos que otra comprobación de cancelación la detuviera. Del mismo modo, asignar nil a la variable del propietario no es una solicitud de cancelación.

Para una vista previa de escritorio, estas distinciones ofrecen tres controles útiles: si el propietario sigue asignado, qué valores de entrada usa el cálculo y si la operación debe detenerse. Elige cada uno explícitamente en lugar de esperar que una captura débil implemente los tres.

¿Qué deberías cambiar para una vista previa de escritorio?

Usa la versión débil cuando una vista previa pendiente solo tenga valor mientras su propietario siga disponible. Usa la versión de la instantánea cuando las entradas sintéticas seleccionadas deban producir un resultado de forma independiente. Usa la versión fuerte cuando completar la operación requiera legítimamente al propietario.

El ciclo de vida del objeto también es distinto de la visibilidad de la interfaz. Que se cierre una ventana no demuestra por sí solo que se hayan eliminado todas las referencias fuertes a su calculadora. Si la aplicación debe detener el trabajo pendiente cuando se cierra una vista previa, conecta ese evento de la aplicación con el método cancel() del identificador de tarea retenido; el cálculo aún debe cooperar con la cancelación. Tarea

Mantén pequeña la carga de trabajo de este ejemplo. El aislamiento del actor principal simplifica el acceso al estado y la espera suspende la tarea, pero la suma síncrona sigue ejecutándose en ese actor. La guía de concurrencia de Swift explica que el actor principal también se ocupa del trabajo de la interfaz. Un cálculo considerablemente mayor requiere un diseño de ejecución aparte que pase valores adecuados al trabajador. Concurrencia

Para este cálculo local, la prueba decisiva es sencilla: elimina la referencia de quien llama antes de que se ejecute la operación diferida y observa el resultado. La captura fuerte conserva al propietario, la captura débil permite un resultado explícito de indisponibilidad y una instantánea de una matriz de enteros conserva solo las entradas necesarias para terminar.

Fuentes