Un calcolo Swift ritardato mantiene in vita il suo proprietario quando il task lo acquisisce con un riferimento forte. Usa un’acquisizione debole per consentire al proprietario di scomparire durante l’attesa, oppure acquisisci uno snapshot indipendente dei valori quando il calcolo deve terminare senza di esso.

Per uno sviluppatore Mac che realizza una piccola anteprima di calcolo desktop, il compito pratico è scegliere deliberatamente tale durata. Questo esempio attende brevemente, somma i valori sintetici [4, 8, 12] e formatta il totale. Tutto viene eseguito localmente in memoria.

Quale durata dovrebbe avere il tuo calcolo?

Le liste di acquisizione di Swift consentono di specificare cosa mantiene una closure. Un’acquisizione forte mantiene un riferimento all’istanza; un riferimento debole consente che venga deallocata e in seguito diventa nil. Si tratta di comportamenti documentati del linguaggio, indipendenti dalla durata dell’attesa del calcolo. Automatic Reference Counting

Scegli il contratto prima della sintassi di acquisizione. La seguente tabella decisionale applica queste regole del linguaggio a un’anteprima desktop ritardata:

Comportamento desiderato Strategia di acquisizione Cosa succede dopo che il chiamante rilascia il proprietario?
Terminare usando lo stesso proprietario [self] Il task mantiene il proprietario disponibile per il suo lavoro.
Continuare solo se il proprietario è ancora disponibile [weak self] Dopo l’attesa, il task verifica se il proprietario esiste ancora.
Terminare usando i valori selezionati al momento della pianificazione [samples = self.samples] Il task conserva i valori acquisiti senza mantenere questo proprietario.

La riga dello snapshot ha una condizione essenziale: questo esempio acquisisce un array di interi. Le espressioni nelle liste di acquisizione vengono valutate quando la closure viene creata. Acquisire invece un’istanza di classe manterrebbe un riferimento a quell’istanza; non creerebbe una copia indipendente dei suoi contenuti mutabili. Expressions

C’è anche una questione distinta relativa ai valori. Le versioni con acquisizione forte e debole leggono i valori del proprietario dopo l’attesa. La versione con snapshot usa i valori presenti al momento della pianificazione. Se gli input dell’anteprima possono cambiare durante l’attesa, decidi quale interpretazione deve rappresentare il risultato.

Esegui un confronto locale completo sul tuo Mac

Usa macOS 13 o versioni successive e una toolchain Swift 6 o successiva installata. L’overload di Task.sleep(for:) basato sulla durata usato qui è documentato a partire da macOS 13. Sospende il task senza bloccare il thread sottostante e genera CancellationError se la cancellazione avviene prima della fine dell’attesa. sleep(for:tolerance:clock:)

Salva il seguente programma completo come CaptureDemo.swift. La calcolatrice, le closure dei task e la dimostrazione vengono eseguite su @MainActor, che serializza l’accesso al loro stato. Swift documenta sia l’isolamento dell’attore principale a livello di classe, sia l’annotazione esplicita della closure usata qui. Concurrency

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)
    }
}

L’aritmetica usa reduce(0, +) della libreria standard per combinare gli interi in un’unica somma. NumberFormatter di Foundation trasforma tale somma in testo da visualizzare; il suo metodo string(from:) restituisce una stringa opzionale, quindi il programma segnala esplicitamente un errore di formattazione. Il campione fisso dà come risultato 24. reduce(::), NumberFormatter, string(from:)

Nel Terminale, spostati nella cartella che contiene il file ed esegui questi comandi. Il target specifica macOS 13 per l’architettura del processore del tuo Mac. L’opzione -parse-as-library consente di usare il punto di ingresso @main esplicito del file. Il compilatore Swift accetta file sorgente e opzioni da riga di comando; swiftc -help elenca le opzioni installate con la tua toolchain. Swift command-line manual

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

Di seguito è mostrato l’output atteso dell’applicazione. È ricavato dalle regole di acquisizione e cancellazione; l’ambiente di ricerca di questo manoscritto non disponeva di un compilatore Swift, quindi il programma è stato esaminato ma non eseguito.

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 probe debole osserva la proprietà senza aggiungere un altro riferimento forte. Ogni dimostrazione elimina il proprio riferimento forte prima di attendere il task. Poiché la dimostrazione e le closure dei task usano lo stesso attore, la closure appena creata non può interferire con quella configurazione sincrona. In questo modo il confronto non dipende da supposizioni sulla rapidità con cui parte il task. Concurrency

Perché è importante la posizione del controllo del riferimento debole?

Nel ramo debole, guard let self compare dopo l’attesa. Il task può quindi attendere senza mantenere il proprietario con un riferimento forte. Se il proprietario è scomparso, il risultato esplicito Owner unavailable termina il calcolo.

Spostare lo spacchettamento prima dell’attesa cambia la durata. Il frammento seguente è illustrativo e sostituisce il corpo della closure del ramo debole; non è un programma a sé stante:

// 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)

Qui, il binding opzionale riuscito crea un riferimento locale forte, necessario di nuovo dopo la sospensione. Di conseguenza, una volta riuscito quel binding, il proprietario rimane disponibile durante l’attesa. Questo deriva dalle regole di Swift sui riferimenti forti: l’acquisizione debole determina come la closure raggiunge inizialmente il proprietario, mentre la variabile locale spacchettata determina come il codice successivo lo mantiene. Automatic Reference Counting

Per il contratto «attendere senza mantenere il proprietario, poi calcolare solo se esiste ancora», mantieni lo spacchettamento dopo l’attesa ed esegui subito dopo il piccolo calcolo. Ulteriori punti di sospensione dopo lo spacchettamento richiedono un’altra decisione sulla durata.

Un’acquisizione unowned richiede che l’istanza referenziata sia valida ogni volta che viene consultata. Non fornisce il controllo opzionale di assenza necessario in questo esempio, dove è intenzionale che il proprietario possa essere rilasciato durante l’attesa. Automatic Reference Counting

Rilasciare l’handle del task interrompe il calcolo?

Un task continua a essere eseguito anche se ne scarti l’handle. Conservare l’handle consente di attenderne il risultato o richiedere la cancellazione. Apple documenta inoltre che un task completato rilascia prontamente la propria closure operativa e i riferimenti che questa manteneva. Un’acquisizione forte di durata finita estende quindi la vita del proprietario per il tempo necessario al lavoro, senza necessariamente creare un ciclo di riferimenti persistente. Task

La cancellazione è un contratto distinto dalla proprietà. L’ultima dimostrazione richiede la cancellazione tramite l’handle. L’attesa che può generare un errore e il successivo Task.checkCancellation() offrono punti in cui l’operazione può interrompersi, e il chiamante segnala l’esito. La cancellazione in Swift è cooperativa: richiederla non interrompe forzatamente codice di calcolo arbitrario. Task

Non sostituire l’attesa che genera un errore con try? per poi proseguire senza condizioni, se la cancellazione deve impedire un risultato. Sopprimere quell’errore consentirebbe di raggiungere il calcolo, a meno che un altro controllo di cancellazione non lo fermi. Allo stesso modo, assegnare nil alla variabile del proprietario non costituisce una richiesta di cancellazione.

Per un’anteprima desktop, queste distinzioni offrono tre controlli utili: se il proprietario rimane allocato, quali valori di input usa il calcolo e se l’operazione deve interrompersi. Scegli ciascuno in modo esplicito, invece di aspettarti che un’acquisizione debole li gestisca tutti.

Cosa dovresti modificare per un’anteprima desktop?

Usa la versione debole quando un’anteprima in attesa è utile solo finché il proprietario resta disponibile. Usa la versione con snapshot quando gli input sintetici selezionati devono produrre un risultato in modo indipendente. Usa la versione forte quando per completare legittimamente l’operazione è necessario il proprietario.

La durata di un oggetto è distinta anche dalla visibilità dell’interfaccia. La chiusura di una finestra non dimostra di per sé che siano stati rimossi tutti i riferimenti forti alla sua calcolatrice. Se l’applicazione deve interrompere il lavoro in sospeso alla chiusura di un’anteprima, collega quell’evento dell’applicazione al metodo cancel() dell’handle del task conservato; il calcolo deve comunque collaborare con la cancellazione. Task

Mantieni ridotto il carico di lavoro di questo esempio. L’isolamento dell’attore principale rende semplice l’accesso allo stato e l’attesa sospende l’esecuzione, ma la somma sincrona viene comunque eseguita su quell’attore. La guida alla concorrenza di Swift spiega che l’attore principale gestisce anche il lavoro dell’interfaccia. Un calcolo molto più grande richiede una progettazione di esecuzione separata, che passi valori adatti al worker. Concurrency

Per questo calcolo locale, la verifica decisiva è semplice: rimuovi il riferimento del chiamante prima che venga eseguita l’operazione ritardata e osserva il risultato. L’acquisizione forte preserva il proprietario, quella debole consente un esito esplicito di indisponibilità e uno snapshot di un array di interi conserva solo gli input necessari per terminare.

Fonti