cat swift/cancellation-progettare-interruzione.md
Cancellation: progettare l'interruzione
La cancellazione non è un dettaglio: un task deve sapere quando fermarsi e come lasciare il sistema in uno stato coerente.
In Swift, cancellare un task non significa ucciderlo immediatamente. La cancellazione è cooperativa: il task riceve un segnale e deve controllarlo nei punti opportuni.
Questo è fondamentale per UI, networking, ricerca, scroll, retry, download e task lunghi.
Cancellare un task
let task = Task {
try await caricaRisultati()
}
task.cancel()
Il task viene marcato come cancellato. Il codice dentro deve rispettare la cancellazione.
Task.isCancelled
func elabora(_ elementi: [Elemento]) async -> [Risultato] {
var risultati: [Risultato] = []
for elemento in elementi {
if Task.isCancelled {
return risultati
}
risultati.append(await trasforma(elemento))
}
return risultati
}
Questa forma è utile quando vuoi uscire in modo controllato.
Task.checkCancellation
Se vuoi lanciare un errore:
func elabora(_ elementi: [Elemento]) async throws -> [Risultato] {
var risultati: [Risultato] = []
for elemento in elementi {
try Task.checkCancellation()
risultati.append(await trasforma(elemento))
}
return risultati
}
Task.checkCancellation() lancia CancellationError se il task è cancellato.
Gestire CancellationError
La cancellazione spesso non è un errore da mostrare all’utente.
do {
let risultati = try await cerca(query)
mostra(risultati)
} catch is CancellationError {
// L'utente ha cambiato query o schermata: nessun messaggio.
} catch {
mostraErrore(error)
}
Questo evita toast o alert inutili quando l’utente sta semplicemente digitando.
Ricerca con cancellazione
final class SearchViewModel {
private var searchTask: Task<Void, Never>?
func cerca(_ query: String) {
searchTask?.cancel()
searchTask = Task {
do {
try await Task.sleep(for: .milliseconds(300))
let risultati = try await api.cerca(query)
await aggiornaUI(risultati)
} catch is CancellationError {
return
} catch {
await mostraErrore(error)
}
}
}
}
Ogni nuova query cancella la precedente. Il debounce con Task.sleep è cancellabile.
Cleanup
Se un task gestisce risorse, deve lasciarle in uno stato coerente.
func scaricaFile() async throws {
let file = try FileTemporaneo()
do {
try await download(in: file)
try file.spostaADestinazioneFinale()
} catch {
file.eliminaSeEsiste()
throw error
}
}
La cancellazione passa dal catch come errore.
Cancellazione nei task group
Se il task padre viene cancellato, anche i figli dovrebbero ricevere cancellazione.
Questo è un vantaggio della structured concurrency. Ma i figli devono comunque controllare cancellazione se fanno lavoro lungo.
Retry e cancellazione
Un retry deve rispettare cancellazione:
func retry<T>(
tentativi: Int,
operazione: () async throws -> T
) async throws -> T {
var ultimoErrore: Error?
for _ in 0..<tentativi {
try Task.checkCancellation()
do {
return try await operazione()
} catch is CancellationError {
throw CancellationError()
} catch {
ultimoErrore = error
try await Task.sleep(for: .milliseconds(250))
}
}
throw ultimoErrore ?? APIError.sconosciuto
}
Non ritentare dopo cancellazione.
Checklist
Quando scrivi codice async, chiediti:
- chi può cancellare il task?
- dove controllo cancellazione?
- devo restituire parziale o lanciare?
CancellationErrorva mostrato all’utente?- retry e sleep rispettano cancellazione?
- le risorse vengono pulite?
- i task figli sono strutturati?
Esercizio
Implementa una ricerca prodotti:
- ogni query cancella la precedente;
- aggiungi debounce con
Task.sleep; - ignora
CancellationError; - mostra gli altri errori;
- cancella il task in
deinit.
Poi scrivi una nota: cosa succede se l’utente digita rapidamente 5 query?