cat swift/value-types-reference-types.md

Lezione 02 35 min

Value types e reference types

La distinzione più importante per capire Swift: quando un dato viene copiato e quando viene condiviso.

Swift è spesso descritto come un linguaggio che preferisce i value types. Questa frase è vera, ma può diventare vaga se non la colleghiamo a un problema pratico.

La domanda concreta è: quando passo un dato a un’altra parte del programma, sto passando una copia indipendente oppure sto condividendo la stessa identità?

Questa differenza cambia il modo in cui ragioni su bug, mutabilità, concorrenza e architettura.

Il problema della condivisione invisibile

Immagina un carrello:

struct Carrello {
  var prodotti: [String]
}

var carrelloA = Carrello(prodotti: ["Libro"])
var carrelloB = carrelloA

carrelloB.prodotti.append("Mouse")

print(carrelloA.prodotti)
print(carrelloB.prodotti)

Con una struct, carrelloB parte come copia di carrelloA. Quando modifichi carrelloB, non stai modificando carrelloA.

Questo è il comportamento mentale dei value types: due valori uguali possono evolvere separatamente.

Struct: identità del valore

Una struct è adatta quando ciò che conta è il contenuto.

Due prodotti con gli stessi dati sono, dal punto di vista del dominio, lo stesso valore:

struct Prodotto: Equatable {
  let id: String
  let nome: String
  let prezzoInCent: Int
}

let a = Prodotto(id: "p-1", nome: "Libro", prezzoInCent: 1900)
let b = Prodotto(id: "p-1", nome: "Libro", prezzoInCent: 1900)

print(a == b) // true

Qui non ci interessa dove vivono in memoria. Ci interessa che rappresentino la stessa informazione.

Class: identità dell’oggetto

Una class, invece, ha identità di riferimento. Due variabili possono puntare allo stesso oggetto.

final class SessioneUtente {
  var token: String

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

let sessioneA = SessioneUtente(token: "abc")
let sessioneB = sessioneA

sessioneB.token = "xyz"

print(sessioneA.token) // xyz

sessioneA e sessioneB non sono due sessioni indipendenti. Sono due riferimenti alla stessa sessione.

Questo comportamento è utile quando hai bisogno di identità condivisa, ma è pericoloso se lo usi senza pensarci.

Quando preferire struct

In Swift, parti quasi sempre da struct quando stai modellando dati.

Buoni candidati per struct:

  • Prodotto
  • Indirizzo
  • Prezzo
  • RigaOrdine
  • Configurazione
  • FiltroRicerca
  • Coordinate

Il motivo non è ideologico. È pratico: i value types riducono la condivisione accidentale.

Quando usare class

Una class è utile quando ti serve identità condivisa o integrazione con API che la richiedono.

Buoni candidati per class:

  • un oggetto che rappresenta una sessione viva;
  • un controller o coordinatore con stato condiviso;
  • un wrapper per risorse esterne;
  • un oggetto che deve essere osservato da più parti;
  • un tipo che deve ereditare da una classe di framework.

In altre parole: usa class quando l’identità dell’oggetto conta.

Il test mentale

Quando devi scegliere tra struct e class, chiediti:

Se passo questo valore a un’altra funzione è quella funzione lo modifica, mi aspetto che il chiamante veda la modifica?

Se la risposta è no, probabilmente vuoi una struct.

Se la risposta è sì, forse vuoi una class, oppure un altro modello esplicito di condivisione.

Esempio nel dominio carrello

Un prodotto è un value type:

struct Prodotto {
  let id: String
  let nome: String
  let prezzoInCent: Int
}

Una riga del carrello è un value type:

struct RigaCarrello {
  let prodotto: Prodotto
  var quantita: Int
}

Il carrello stesso può essere un value type:

struct Carrello {
  var righe: [RigaCarrello]

  var totaleInCent: Int {
    righe.reduce(0) { totale, riga in
      totale + riga.prodotto.prezzoInCent * riga.quantita
    }
  }
}

Questa scelta rende il carrello facile da testare: dato un insieme di righe, il totale e deterministico.

Errore comune

Un errore frequente e usare una class perché “poi devo modificarla”.

La modificabilità non richiede una classe. Una struct può avere proprietà var:

struct Contatore {
  var valore: Int

  mutating func incrementa() {
    valore += 1
  }
}

La parola chiave mutating rende esplicito che il metodo cambia il valore. Anche questo e Swift: quando qualcosa cambia, il linguaggio vuole che si veda.

Checklist

Prima di scegliere class, controlla:

  • mi serve davvero identità condivisa?
  • voglio che più parti osservino lo stesso stato?
  • sto interoperando con un framework che richiede una classe?
  • sto usando ereditarietà per un motivo reale?

Se non hai almeno una risposta forte, parti da struct.

Esercizio

Nel dominio carrello, crea questi tipi:

  • Prodotto
  • RigaCarrello
  • Carrello
  • SessioneCheckout

Decidi quali devono essere struct e quali class. Per ogni scelta scrivi una frase: “Uso questo tipo perché…”.

Poi crea due copie di un carrello e modifica solo la seconda. Verifica che la prima resti invariata.