cat swift/retain-cycle-weak-unowned.md

Lezione 25 50 min

Retain cycle, weak è unowned

I cicli di riferimento sono il classico bug ARC: capire quando usare weak e quando unowned evita memory leak difficili da vedere.

Un retain cycle nasce quando due o più oggetti si mantengono vivi a vicenda con riferimenti forti. ARC non può liberarli, perché ciascuno ha ancora almeno un proprietario.

Questo è uno dei bug di memoria più comuni nel codice Swift basato su classi.

Ciclo semplice

final class Utente {
  let nome: String
  var sessione: SessioneCheckout?

  init(nome: String) {
    self.nome = nome
  }

  deinit {
    print("Utente rilasciato")
  }
}

final class SessioneCheckout {
  var utente: Utente?

  deinit {
    print("Sessione rilasciata")
  }
}

Uso:

var utente: Utente? = Utente(nome: "Ada")
var sessione: SessioneCheckout? = SessioneCheckout()

utente?.sessione = sessione
sessione?.utente = utente

utente = nil
sessione = nil

Nessun deinit. Gli oggetti si tengono vivi a vicenda.

weak

Un riferimento weak non mantiene vivo l’oggetto.

final class SessioneCheckout {
  weak var utente: Utente?

  deinit {
    print("Sessione rilasciata")
  }
}

Ora Utente può possedere SessioneCheckout, ma la sessione non possiede l’utente. Il ciclo si rompe.

Le proprietà weak devono essere opzionali, perché l’oggetto referenziato può sparire.

Quando usare weak

Usa weak quando:

  • il riferimento non deve possedere l’oggetto;
  • l’oggetto referenziato può sparire;
  • hai relazioni delegate;
  • hai riferimenti inversi parent-child;
  • vuoi evitare cicli con view model, coordinator o controller.

Esempio delegate:

protocol CheckoutDelegate: AnyObject {
  func checkoutCompletato()
}

final class CheckoutCoordinator {
  weak var delegate: CheckoutDelegate?
}

AnyObject limita il protocollo alle classi, necessario per weak.

unowned

unowned non mantiene vivo l’oggetto, ma non è opzionale.

final class RigaVista {
  unowned let tabella: TabellaVista

  init(tabella: TabellaVista) {
    self.tabella = tabella
  }
}

Usalo solo quando sei certo che l’oggetto referenziato vivra almeno quanto chi lo referenzia.

Se accedi a un riferimento unowned dopo che l’oggetto è stato rilasciato, il programma crasha.

weak vs unowned

Usa weak come default per rompere cicli.

Valuta unowned solo quando:

  • la relazione è obbligatoria;
  • il ciclo di vita e garantito;
  • un optional sarebbe semanticamente sbagliato;
  • sei sicuro della durata.

Molti bug evitabili nascono da unowned usato con troppa fiducia.

Cicli con closure

I retain cycle non nascono solo tra oggetti. Possono nascere anche con closure.

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

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

  func aggiorna() {}
}

ViewModel mantiene la closure. La closure cattura self. Ciclo.

Soluzione:

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

La prossima lezione approfondisce le capture list.

Debug con deinit

In laboratorio, aggiungi:

deinit {
  print("NomeClasse rilasciata")
}

Poi crea un ciclo e verifica che il messaggio non appaia. Rompi il ciclo con weak e verifica che appaia.

Checklist

Quando vedi due classi collegate:

  • entrambe hanno riferimenti forti?
  • una relazione e parent-child?
  • esiste un delegate?
  • una closure cattura self?
  • deinit viene chiamato?
  • weak renderebbe il ciclo corretto?
  • unowned è davvero garantito?

Esercizio

Crea:

  • CheckoutCoordinator;
  • CheckoutViewModel;
  • CheckoutDelegate.

Prima crea un ciclo forte e osserva che deinit non viene chiamato.

Poi correggi:

  • delegate con weak;
  • closure con [weak self];
  • commenta se useresti mai unowned in questo scenario.