cat swift/closure-capture-liste-cattura.md

Lezione 26 45 min

Closure capture e liste di cattura

Le closure possono catturare valori e oggetti: capire capture list, self, weak e snapshot evita bug sottili.

Una closure può usare valori dichiarati fuori dal suo corpo. Quando lo fa, li cattura.

let prefisso = "Totale"

let descrivi = { (valore: Int) in
  "\(prefisso): \(valore)"
}

La closure cattura prefisso.

Questo è comodo, ma quando la closure cattura oggetti o stato mutabile possono nascere bug.

Cattura di valori

var totale = 100

let stampa = {
  print(totale)
}

totale = 200
stampa() // 200

La closure vede il valore aggiornato della variabile catturata.

Se vuoi catturare uno snapshot:

var totale = 100

let stampa = { [totale] in
  print(totale)
}

totale = 200
stampa() // 100

La capture list [totale] cattura il valore al momento della creazione della closure.

Cattura di self

Dentro una classe:

final class ViewModel {
  var onUpdate: (() -> Void)?

  func configura() {
    onUpdate = {
      self.aggiorna()
    }
  }

  func aggiorna() {}
}

La closure cattura self fortemente. Se self mantiene la closure, hai un ciclo.

weak self

onUpdate = { [weak self] in
  self?.aggiorna()
}

self diventa opzionale dentro la closure. Se l’oggetto è stato rilasciato, la closure non fa nulla.

Questa è la forma più comune per callback che possono vivere quanto o più dell’oggetto.

guard let self

Quando vuoi evitare optional chaining ripetuto:

onUpdate = { [weak self] in
  guard let self else {
    return
  }

  self.aggiorna()
  self.salvaStato()
}

Questa forma mantiene self vivo solo durante l’esecuzione della closure.

unowned self

onUpdate = { [unowned self] in
  self.aggiorna()
}

Usalo solo quando sei certo che la closure non verrà chiamata dopo la deallocazione di self.

Se sbagli, crash.

Nel dubbio, usa weak.

Capture list con proprietà specifiche

Non devi catturare per forza tutto self.

final class Presenter {
  let formatter: PrezzoFormatter

  init(formatter: PrezzoFormatter) {
    self.formatter = formatter
  }

  func render(prezzi: [Prezzo]) -> [String] {
    prezzi.map { [formatter] prezzo in
      formatter.stringa(da: prezzo)
    }
  }
}

Qui catturi formatter, non l’intero oggetto.

Questo riduce accoppiamento e rischi.

Closure escaping

Una closure escaping sopravvive alla chiamata della funzione.

func carica(completamento: @escaping () -> Void) {
  self.completamento = completamento
}

Le closure escaping sono più rischiose per i retain cycle, perché possono essere conservate.

Con async/await userai meno callback, ma le closure restano ovunque: UI, Combine, timer, gesture, sorting, map/filter.

Timer e task ripetuti

Un caso tipico:

timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
  self.tick()
}

Se il timer è mantenuto da self è la closure cattura self, hai un ciclo.

timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
  self?.tick()
}

Checklist

Quando scrivi una closure, chiediti:

  • cattura self?
  • la closure viene salvata da self?
  • la closure è escaping?
  • può essere chiamata dopo la deallocazione dell’oggetto?
  • posso catturare una proprietà invece di tutto self?
  • serve uno snapshot di un valore?
  • weak è sufficiente?

Esercizio

Crea una classe CouponViewModel con:

  • una proprietà onChange: (() -> Void)?;
  • una funzione configuraCallback;
  • una closure che inizialmente cattura self forte;
  • un deinit con print.

Verifica il ciclo, poi correggilo con [weak self].

Infine crea un esempio con [totale] per catturare uno snapshot di un valore.