cat swift/stack-heap-semantica-pratica.md

Lezione 2240 min

Stack, heap e semantica pratica

Stack e heap spiegati dal punto di vista del programmatore Swift: cosa serve sapere per ragionare su valori, riferimenti e performance.

Stack e heap sono parole che spesso fanno sembrare la memoria più misteriosa di quanto serva. In questo corso non ci interessa diventare compilatori umani. Ci interessa capire abbastanza da ragionare su semantica, bug e prestazioni.

Swift ti permette di scrivere molto codice senza pensare direttamente alla memoria. Ma alcune scelte, come struct o class, hanno conseguenze.

Il modello mentale minimo

Lo stack è una zona di memoria usata per dati con vita breve e prevedibile: chiamate di funzione, variabili locali, valori temporanei.

L’heap è una zona usata per dati con vita meno prevedibile, spesso condivisi o gestiti tramite riferimenti.

Questa non è una mappa completa di come Swift ottimizza realmente il programma, ma è un buon modello mentale.

Value semantics

Quando usi una struct, ragioni in termini di valore.

struct Punto {
  var x: Int
  var y: Int
}

var a = Punto(x: 0, y: 0)
var b = a

b.x = 10

print(a.x) // 0
print(b.x) // 10

a e b sono valori indipendenti dal punto di vista semantico. Swift può ottimizzare sotto, ma tu ragioni così.

Reference semantics

Con una class, ragioni in termini di riferimento.

final class Sessione {
  var token: String

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

let a = Sessione(token: "abc")
let b = a

b.token = "xyz"

print(a.token) // xyz

a e b puntano allo stesso oggetto. La modifica è condivisa.

Lo stack non è sinonimo di struct

Una semplificazione comune è: struct sullo stack, class sull’heap.

È una scorciatoia utile all’inizio, ma non è sempre vera. Swift può decidere ottimizzazioni diverse. Una struct può contenere riferimenti a memoria heap, per esempio un Array o una String.

struct Catalogo {
  var prodotti: [Prodotto]
}

Catalogo è un value type, ma l’array usa storage interno gestito in modo efficiente. La semantica resta di valore, anche se l’implementazione può usare heap.

La cosa importante è separare:

  • semantica: come devi ragionare;
  • implementazione: come Swift organizza la memoria.

Semantica prima dell’ottimizzazione

Non scegliere class pensando “così evito copie” prima di aver misurato.

struct Ordine {
  let righe: [RigaOrdine]
  let totale: Prezzo
}

Questa struct è probabilmente una scelta molto buona. Anche se contiene un array, Swift usa copy-on-write per evitare copie costose finché possibile.

Scegli il modello giusto per il dominio. Poi misura se c’è un problema.

Vita dei valori

Una variabile locale vive finché serve alla funzione:

func creaTotale(righe: [RigaCarrello]) -> Int {
  let subtotali = righe.map(\.subtotaleInCent)
  return subtotali.reduce(0, +)
}

subtotali non dovrebbe sopravvivere oltre la funzione. Swift può ottimizzare molto questo tipo di codice.

Un oggetto condiviso può vivere molto più a lungo:

final class CacheProdotti {
  var prodotti: [String: Prodotto] = [:]
}

La cache vive finché esiste almeno un riferimento forte a essa.

Debug mentale

Quando qualcosa cambia “da solo”, chiediti:

  • sto usando una class condivisa?
  • ho passato un riferimento a un altro oggetto?
  • una closure sta catturando stato?
  • una cache o singleton sta mantenendo dati vivi?

Quando qualcosa sembra costoso, chiediti:

  • sto copiando una collection molto grande?
  • sto trasformando stringhe molte volte?
  • sto creando oggetti in un ciclo caldo?
  • ho misurato o sto immaginando?

Esempio: carrello snapshot

Un carrello come value type è ottimo per creare snapshot.

struct Carrello {
  let righe: [RigaCarrello]

  func aggiungendo(_ riga: RigaCarrello) -> Carrello {
    Carrello(righe: righe + [riga])
  }
}

Ogni versione del carrello è un valore. Questo rende più facile testare, annullare modifiche, confrontare stati.

Una sessione checkout, invece, può essere una classe:

final class SessioneCheckout {
  private(set) var carrello: Carrello

  init(carrello: Carrello) {
    self.carrello = carrello
  }

  func aggiorna(_ nuovoCarrello: Carrello) {
    carrello = nuovoCarrello
  }
}

La sessione ha identità e stato nel tempo.

Checklist

Quando ragioni su stack/heap in Swift:

  • parti dalla semantica, non dalla posizione fisica in memoria;
  • usa struct per valori;
  • usa class quando serve identità condivisa;
  • ricorda che collection e stringhe hanno storage ottimizzato;
  • misura prima di cambiare modello per performance;
  • sospetta condivisione quando una modifica appare altrove.

Esercizio

Crea due versioni di una SessioneCheckout:

  1. struct immutabile che restituisce nuove versioni;
  2. class con stato aggiornabile.

Poi scrivi un esempio in cui due variabili si comportano diversamente nelle due versioni. Spiega quale useresti in un dominio puro e quale in un flusso UI.