cat swift/laboratorio-leak-copie-performance.md

Lezione 28 75 min

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:

  • CouponRepository mantiene CouponEngine;
  • CouponEngine mantiene CouponRepository;
  • CouponEngine mantiene una closure;
  • la closure cattura self fortemente;
  • 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 self debolmente.

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:

  • deinit viene chiamato per gli oggetti che devono morire?
  • ogni relazione forte rappresenta vero ownership?
  • ogni weak ha 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.