cat swift/laboratorio-leak-copie-performance.md
Laboratorio: diagnosticare leak e copie inutili
Laboratorio del Modulo 4: partire da una piccola libreria Swift con leak, capture sbagliate e inefficienze, poi rifattorizzarla.
Questo laboratorio chiude il modulo su memoria, performance è semantica. Partiamo da una piccola libreria volutamente problematica è la miglioriamo.
Non useremo strumenti avanzati subito. Prima alleniamo l’occhio: riconoscere classi condivise, cicli, closure che catturano self, copie e allocazioni evitabili.
Codice iniziale problematico
final class CouponRepository {
var engine: CouponEngine?
var coupon: [Coupon] = []
deinit {
print("CouponRepository deinit")
}
}
final class CouponEngine {
var repository: CouponRepository?
var onUpdate: (() -> Void)?
deinit {
print("CouponEngine deinit")
}
func start() {
onUpdate = {
self.ricarica()
}
}
func ricarica() {}
}
Problemi:
CouponRepositorymantieneCouponEngine;CouponEnginemantieneCouponRepository;CouponEnginemantiene una closure;- la closure cattura
selffortemente; - gli oggetti potrebbero non essere mai rilasciati.
Passo 1: osservare il leak
var repository: CouponRepository? = CouponRepository()
var engine: CouponEngine? = CouponEngine()
repository?.engine = engine
engine?.repository = repository
engine?.start()
repository = nil
engine = nil
Se deinit non stampa nulla, hai un ciclo.
Passo 2: chiarire ownership
Chi deve possedere chi?
Una possibile scelta:
- il service possiede repository ed engine;
- engine non deve possedere repository in modo forte se non serve;
- eventuali callback devono catturare
selfdebolmente.
Correzione:
final class CouponEngine {
weak var repository: CouponRepository?
var onUpdate: (() -> Void)?
deinit {
print("CouponEngine deinit")
}
func start() {
onUpdate = { [weak self] in
self?.ricarica()
}
}
func ricarica() {}
}
Ora il ciclo repository-engine può essere rotto. Valuta anche se engine debba conoscere davvero il repository: forse la dipendenza andrebbe invertita.
Passo 3: passare a composizione più chiara
Un design migliore:
struct CouponService {
let repository: CouponRepositoryProtocol
let engine: CouponEngine
}
E se CouponEngine non ha bisogno di identità:
struct CouponEngine {
func applica(_ coupon: Coupon, a carrello: Carrello) throws -> Carrello {
...
}
}
Meno classi, meno ARC, meno cicli.
Passo 4: copie e collection
Codice inefficiente:
func descrizioneProdotti(_ prodotti: [Prodotto]) -> String {
var risultato = ""
for prodotto in prodotti {
risultato += "\(prodotto.nome)\n"
}
return risultato
}
Refactor:
func descrizioneProdotti(_ prodotti: [Prodotto]) -> String {
prodotti
.map(\.nome)
.joined(separator: "\n")
}
Oppure:
func descrizioneProdotti(_ prodotti: [Prodotto]) -> String {
var righe: [String] = []
righe.reserveCapacity(prodotti.count)
for prodotto in prodotti {
righe.append(prodotto.nome)
}
return righe.joined(separator: "\n")
}
Passo 5: lookup ripetuti
Codice sospetto:
func prodottiPerOrdine(ids: [String], catalogo: [Prodotto]) -> [Prodotto] {
ids.compactMap { id in
catalogo.first { prodotto in
prodotto.id == id
}
}
}
Se ids e catalogo sono grandi, fai molte ricerche lineari.
Refactor:
func prodottiPerOrdine(ids: [String], catalogo: [Prodotto]) -> [Prodotto] {
let prodottiPerId = Dictionary(uniqueKeysWithValues: catalogo.map { prodotto in
(prodotto.id, prodotto)
})
return ids.compactMap { id in
prodottiPerId[id]
}
}
Passo 6: pipeline o ciclo?
Pipeline:
let totale = righe
.filter { $0.prodotto.categoria == .libri }
.map { $0.subtotaleInCent }
.reduce(0, +)
Ciclo:
var totale = 0
for riga in righe where riga.prodotto.categoria == .libri {
totale += riga.subtotaleInCent
}
Entrambe possono essere idiomatiche. Se il codice è nel dominio e leggibilità conta, la pipeline è ottima. Se il codice è in un punto caldo e vuoi evitare intermedi, il ciclo è chiaro ed efficiente.
Passo 7: mini misurazione
Per confronti didattici:
let clock = ContinuousClock()
let start = clock.now
_ = prodottiPerOrdine(ids: ids, catalogo: catalogo)
let end = clock.now
print(end - start)
Non costruire una cultura della micro-ottimizzazione. Usa la misurazione per confermare ipotesi.
Versione finale attesa
Alla fine del laboratorio dovresti avere:
- meno classi dove bastano struct;
- nessun ciclo forte;
- closure con
[weak self]quando necessario; - lookup frequenti convertiti in
Dictionary; - costruzione stringhe senza concatenazione ripetuta;
- collection grandi preallocate dove ha senso;
- motivazioni scritte per ogni scelta.
Checklist finale
Prima di chiudere:
deinitviene chiamato per gli oggetti che devono morire?- ogni relazione forte rappresenta vero ownership?
- ogni
weakha un motivo? - ogni
unowned, se presente, è davvero garantito? - le collection usano la struttura dati adatta?
- le stringhe vengono costruite in modo ragionevole?
- hai misurato almeno un refactor?
Esercizio extra
Prendi una funzione del modulo precedente e cerca:
- una stringa concatenata in loop;
- una ricerca ripetuta con
first; - una closure che cattura
self; - una classe che potrebbe essere una struct.
Rifattorizza e scrivi una breve nota: cosa hai migliorato, e cosa non valeva la pena toccare.