cat swift/errori-comuni-concurrency-swift-6.md

Lezione 37 50 min

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 essere async;
  • catch che non distingue cancellazione;
  • UI aggiornata fuori MainActor;
  • servizi puri annotati @MainActor;
  • classi mutabili condivise;
  • @unchecked Sendable;
  • actor con await e 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?