cat swift/retain-cycle-weak-unowned.md
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? deinitviene chiamato?weakrenderebbe 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
unownedin questo scenario.