cat swift/copy-on-write-senza-magia.md

Lezione 23 45 min

Copy-on-write senza magia

Swift offre semantica di valore con ottimizzazioni intelligenti: capire copy-on-write aiuta a non temere copie e a non abusare delle classi.

Swift incoraggia l’uso dei value types. Una domanda sorge subito: se copio array, stringhe e struct grandi, non sto sprecando memoria?

La risposta breve è: spesso no, grazie al copy-on-write.

Copy-on-write significa: condividi lo storage finché nessuno modifica. Quando arriva una modifica, allora fai una copia se serve.

Il comportamento mentale

var a = [1, 2, 3]
var b = a

b.append(4)

print(a) // [1, 2, 3]
print(b) // [1, 2, 3, 4]

Dal punto di vista del programmatore, a e b sono valori indipendenti.

Sotto, Swift può condividere lo storage dell’array fino alla modifica. Quando b cambia, preserva a.

Semantica semplice, implementazione efficiente

Questo è il bello del copy-on-write: ti permette di ragionare come se ogni copia fosse indipendente, senza pagare sempre il costo di duplicare subito tutta la memoria.

Array, Dictionary, Set e String usano strategie di questo tipo.

var originale = "Swift"
var copia = originale

copia.append(" 6")

print(originale) // Swift
print(copia)     // Swift 6

Quando avviene la copia

La copia dello storage può avvenire quando modifichi un valore che condivide storage con un altro valore.

var primo = Array(0..<10_000)
var secondo = primo

secondo[0] = 999

Qui secondo deve diventare indipendente da primo.

Non devi memorizzare dettagli interni; devi capire la conseguenza: copiare un array non è necessariamente costoso subito, ma modificarlo dopo una condivisione può esserlo.

Evitare classi per paura delle copie

Un errore comune:

final class Carrello {
  var righe: [RigaCarrello]
}

Scelto solo per “non copiare”. Ma così introduci condivisione e mutabilità globale.

Spesso è meglio:

struct Carrello {
  var righe: [RigaCarrello]
}

Poi misuri. Se davvero c’è un problema, ottimizzi con dati alla mano.

isKnownUniquelyReferenced

Puoi implementare copy-on-write nei tuoi tipi usando una classe interna e isKnownUniquelyReferenced.

Esempio didattico:

final class Storage {
  var righe: [RigaCarrello]

  init(righe: [RigaCarrello]) {
    self.righe = righe
  }
}

struct CarrelloCOW {
  private var storage: Storage

  init(righe: [RigaCarrello]) {
    self.storage = Storage(righe: righe)
  }

  var righe: [RigaCarrello] {
    storage.righe
  }

  mutating func aggiungi(_ riga: RigaCarrello) {
    if !isKnownUniquelyReferenced(&storage) {
      storage = Storage(righe: storage.righe)
    }

    storage.righe.append(riga)
  }
}

Questo non è codice da scrivere spesso. Serve a capire il meccanismo: semantica di valore fuori, storage condiviso dentro finché è sicuro.

Quando preoccuparsi

Preoccupati del costo delle copie quando:

  • lavori con collection molto grandi;
  • modifichi spesso copie derivate;
  • sei in un ciclo caldo;
  • i profiler mostrano allocazioni o copie rilevanti;
  • stai costruendo una libreria performance-sensitive.

Non preoccuparti per ogni passaggio di valore in codice applicativo normale.

Segnali di copy-on-write inefficiente

Esempio sospetto:

func aggiungiMolti(_ elementi: [Elemento], a base: [Elemento]) -> [Elemento] {
  var risultato = base

  for elemento in elementi {
    risultato = risultato + [elemento]
  }

  return risultato
}

Meglio:

func aggiungiMolti(_ elementi: [Elemento], a base: [Elemento]) -> [Elemento] {
  var risultato = base
  risultato.reserveCapacity(base.count + elementi.count)
  risultato.append(contentsOf: elementi)
  return risultato
}

Qui riduci riallocazioni e copie intermedie.

Copy-on-write e API design

Se una funzione restituisce una nuova versione di un valore:

func aggiungendo(_ riga: RigaCarrello) -> Carrello

non significa automaticamente che stai copiando tutto in modo ingenuo. Il modello è chiaro; l’implementazione può essere ottimizzata.

Checklist

Ricorda:

  • copy-on-write preserva semantica di valore;
  • copiare una collection non significa sempre duplicare subito;
  • modificare una copia condivisa può causare copia dello storage;
  • non scegliere classi solo per paura delle copie;
  • usa reserveCapacity quando costruisci collection grandi;
  • misura prima di ottimizzare.

Esercizio

Scrivi due funzioni che aggiungono molti elementi a un array:

  1. una con risultato = risultato + [elemento];
  2. una con reserveCapacity e append.

Poi spiega quale versione preferisci e perché. Non serve misurare ancora: per ora vogliamo riconoscere la forma più efficiente.