cat swift/errori-comuni-concurrency-swift-6.md
Errori comuni della concurrency Swift 6
Una raccolta dei problemi più frequenti: task lanciati nel vuoto, cancellazione ignorata, MainActor abusato, actor reentrancy e Sendable silenziato.
Swift 6 rende molti errori concorrenti più visibili. Questo è un bene, ma può sorprendere: codice che prima sembrava innocuo ora genera warning o errori.
Questa lezione raccoglie gli errori più comuni e il modo corretto di ragionarci.
1. Creare Task ovunque
Odore:
func carica() {
Task {
try await api.prodotti()
}
}
Problemi:
- chi aspetta il risultato?
- chi gestisce l’errore?
- chi cancella?
- il chiamante sa che è partito lavoro?
Meglio:
func carica() async throws -> [Prodotto] {
try await api.prodotti()
}
Usa Task al confine UI o sync/async.
2. Ignorare CancellationError
Odore:
do {
prodotti = try await api.prodotti()
} catch {
mostraErrore(error)
}
Se l’utente cancella, potresti mostrare un errore inutile.
Meglio:
do {
prodotti = try await api.prodotti()
} catch is CancellationError {
return
} catch {
mostraErrore(error)
}
3. Aggiornare UI fuori dal MainActor
Odore:
Task {
let prodotti = try await api.prodotti()
self.prodotti = prodotti
}
Se prodotti è stato UI, il view model dovrebbe essere @MainActor o l’aggiornamento dovrebbe entrare in MainActor.
@MainActor
final class ProductViewModel {
var prodotti: [Prodotto] = []
}
4. Mettere tutto su MainActor
Anche l’opposto è sbagliato:
@MainActor
final class APIClient {
func prodotti() async throws -> [Prodotto] { ... }
}
Un client API puro non dovrebbe essere isolato al main actor solo per comodità. Tieni il MainActor vicino alla UI.
5. Condividere classi mutabili
Odore:
final class Cache {
var storage: [String: Prodotto] = [:]
}
Se più task la usano, hai rischio di data race.
Meglio:
actor ProductCache {
private var storage: [String: Prodotto] = [:]
}
6. Usare @unchecked Sendable per zittire
Odore:
final class Cache: @unchecked Sendable {
var storage: [String: Prodotto] = [:]
}
Se non hai sincronizzazione, hai solo nascosto il problema.
@unchecked Sendable richiede responsabilità manuale.
7. Dimenticare actor reentrancy
Odore:
actor Cache {
private var storage: [String: Prodotto] = [:]
func value(for id: String) async throws -> Prodotto {
if let cached = storage[id] {
return cached
}
let loaded = try await api.prodotto(id)
storage[id] = loaded
return loaded
}
}
Durante await, l’actor può eseguire altro. Potresti avviare lo stesso load più volte.
Soluzione possibile: memorizzare task in-flight.
8. Parallelizzare senza limiti
Odore:
try await withThrowingTaskGroup(of: Prodotto.self) { group in
for id in migliaiaDiID {
group.addTask {
try await api.prodotto(id: id)
}
}
}
Potresti saturare rete o server. Usa batch o limite di concorrenza.
9. Modificare stato esterno da task group
Odore:
var prodotti: [Prodotto] = []
group.addTask {
prodotti.append(try await api.prodotto(id: id))
}
I figli dovrebbero restituire valori. Il genitore raccoglie.
10. Nascondere await
Se una funzione può sospendersi, deve essere visibile.
Evita wrapper che fanno partire lavoro asincrono senza comunicarlo nella firma.
func refresh() async throws
è più onesto di:
func refresh()
che crea un task interno.
Checklist finale
Rivedendo codice async, cerca:
Task {}dentro funzioni che potrebbero essereasync;catchche non distingue cancellazione;- UI aggiornata fuori
MainActor; - servizi puri annotati
@MainActor; - classi mutabili condivise;
@unchecked Sendable;- actor con
awaite stato intermedio; - task group illimitati;
- mutazioni di stato esterno nei task figli.
Esercizio
Prendi una funzione async del tuo progetto e fai una code review:
- dove sono gli
await? - chi cancella?
- chi gestisce gli errori?
- quali dati attraversano task o actor?
- cosa dovrebbe essere
Sendable? - cosa dovrebbe essere actor-isolated?
- cosa dovrebbe stare su
MainActor?