1. Value semantics: l'idea
Quando inizi a scrivere codice, e facile pensare cosi: "Se due variabili sembrano uguali, allora in qualche modo sono collegate". E in effetti, in alcuni linguaggi e tipi succede davvero. Ma in Swift, per struct vale di default l'idea di value semantics: un valore si comporta come un numero o come una stringa, cioe come una "cosa" autonoma. Se hai assegnato un valore a un altro, hai ottenuto una copia del valore, e da li in poi vivono vite indipendenti.
Per non farla sembrare filosofia, fissiamo una semplice analogia quotidiana. struct e come una ricetta stampata su un foglio. Se hai fatto una fotocopia della ricetta e sulla copia hai barrato "200 g di zucchero" scrivendo "50 g", il foglio originale a casa nella cartellina non e cambiato. Ed e fantastico: altrimenti tua nonna un giorno si sarebbe ritrovata un borshch dal sapore di cheesecake, e la colpa sarebbe stata tua e dei "riferimenti condivisi".
In modo schematico, si puo immaginare cosi:
flowchart LR
A[Variable a: Book] -->|assignment| B[Variable b: Book]
B -->|change b.title| B2["b: Book (new value)"]
A -->|unchanged| A2["a: Book (old value)"]
2. Copia e immutabilita
Copia all'assegnazione
Il punto piu semplice da cui partire e l'assegnazione. Qui di solito avviene il "crollo delle aspettative": lo studente cambia un campo di b e si aspetta che anche a cambi, perche "sono uguali". Ma con struct non e cosi: a e b sono due valori diversi. Si, inizialmente identici, ma non collegati.
Descriviamo il nostro modello "book" per una mini-biblioteca. Per ora niente complicazioni: solo id, title, author.
import Foundation
struct Book {
let id: Int
var title: String
var author: String
}
Ora guardiamo la copia in modo diretto.
import Foundation
struct Book {
let id: Int
var title: String
var author: String
}
var a = Book(id: 1, title: "Dune", author: "Frank Herbert")
var b = a
b.title = "Dune (Director's Cut?)"
print(a.title) // Dune
print(b.title) // Dune (Director's Cut?)
Nota bene: abbiamo cambiato b.title, ma a.title e rimasto invariato. Questa e value semantics "allo stato puro".
let congela l'istanza nel suo insieme
Un pensiero molto comune tra i principianti e: "Ma dentro il mio struct il campo e var, quindi posso cambiarlo". E Swift risponde tranquillo: "Puoi. Ma solo se l'istanza stessa e var". Se l'istanza e dichiarata con let, diventa completamente immutabile. Non e una punizione, ma una protezione: il valore deve restare un valore, e non trasformarsi in un "oggetto semi-congelato".
Prendiamolo sul serio con un piccolo esempio e fissiamo subito la regola.
import Foundation
struct Book {
let id: Int
var title: String
}
let book = Book(id: 1, title: "Dune")
// book.title = "Dune 2" // ❌ non consentito: l'istanza e dichiarata come let
print(book.title) // Dune
Se devi modificare i campi, dichiara l'istanza come var. Se non serve, let aiuta sia il compilatore sia il tuo cervello: meno punti in cui qualcosa puo "cambiare all'improvviso".
3. Passaggio alle funzioni e restituzione della copia
Perche le modifiche dentro una funzione non sono visibili fuori
Il prossimo punto di sorpresa: passi Book a una funzione, cambi title dentro la funzione, ma fuori non cambia nulla. E anche questo e value semantics: la funzione riceve la propria copia del valore (in senso logico, indipendente), e tu modifichi proprio quella.
In piu c'e un altro dettaglio: i parametri di funzione in Swift, di default, sono immutabili. Quindi non puoi nemmeno scrivere book.title = ... direttamente sul parametro: il compilatore ti fermera. In Swift questo e intenzionale, per non confondere la "modifica locale" con la "modifica dell'argomento del chiamante".
Storicamente, Swift ha persino discusso il fatto che i parametri mutabili (var param) confondono le persone con inout, e si e poi abbandonato questo approccio.
Vediamo un esempio.
import Foundation
struct Book {
let id: Int
var title: String
}
func renameToUppercased(_ book: Book) -> Book {
var copy = book
copy.title = copy.title.uppercased()
return copy
}
let a = Book(id: 1, title: "Dune")
let b = renameToUppercased(a)
print(a.title) // Dune
print(b.title) // DUNE
Qui e importante: non stiamo cercando di "modificare a". Stiamo dicendo onestamente: "dammi il libro — ti restituisco un nuovo libro". Questo e lo stile "tramite copia e ritorno".
Bozza e rollback: uno scenario realistico
Finora abbiamo copiato Book e Library al livello di "una variabile". Nel codice reale e piu comune questo scenario: hai lo stato corrente, fai una "bozza", provi delle modifiche, e poi decidi se accettarle o no.
La value semantics si adatta perfettamente a questo: puoi fare copie liberamente, senza paura di "rompere per sbaglio l'originale".
Mettiamola in scena come una piccola storia: "proviamo a rinominare il libro e annulliamo se il nome risulta vuoto".
import Foundation
struct Book {
let id: Int
var title: String
}
func tryRename(_ book: Book, to newTitle: String) -> Book {
var copy = book
if !newTitle.isEmpty {
copy.title = newTitle
}
return copy
}
let original = Book(id: 1, title: "Dune")
let attempt = tryRename(original, to: "")
print(original.title) // Dune
print(attempt.title) // Dune
Nota bene: abbiamo "giocato" con attempt, ma non abbiamo toccato l'originale. Questo e un modello molto tranquillo: meno sorprese, meno "perche si e cambiato da solo".
4. Come aggiornare i dati: copia o mutazione
Nel codice reale devi continuamente "aggiornare lo stato". E in Swift, con le value semantics, di solito ci sono due strade base: o restituisci una nuova versione del valore (spesso molto leggibile), oppure modifichi la variabile esistente "sul posto" tramite inout.
Entrambi gli approcci sono validi, ma si leggono in modo diverso e hanno rischi differenti.
Per non essere generici, prendiamo la nostra mini-biblioteca: e semplicemente una lista di libri in un array.
import Foundation
struct Book {
let id: Int
var title: String
var author: String
}
struct Library {
var books: [Book]
}
Due varianti: restituire una nuova copia o modificare sul posto
Creiamo ora due varianti per "aggiungere un libro".
Variante A: restituire una nuova copia di Library.
import Foundation
struct Book { let id: Int; var title: String; var author: String }
struct Library { var books: [Book] }
func adding(_ book: Book, to library: Library) -> Library {
var copy = library
copy.books.append(book)
return copy
}
Variante B: modificare la variabile esistente tramite inout.
import Foundation
struct Book { let id: Int; var title: String; var author: String }
struct Library { var books: [Book] }
func addInPlace(_ book: Book, to library: inout Library) {
library.books.append(book)
}
La differenza nell'uso si sente subito.
import Foundation
struct Book { let id: Int; var title: String; var author: String }
struct Library { var books: [Book] }
func adding(_ book: Book, to library: Library) -> Library {
var copy = library
copy.books.append(book)
return copy
}
func addInPlace(_ book: Book, to library: inout Library) {
library.books.append(book)
}
let dune = Book(id: 1, title: "Dune", author: "Frank Herbert")
var lib1 = Library(books: [])
let lib2 = adding(dune, to: lib1)
print(lib1.books.count) // 0
print(lib2.books.count) // 1
addInPlace(dune, to: &lib1)
print(lib1.books.count) // 1
Nota il & con inout: non e una decorazione, ma un marcatore speciale che dice "attenzione, ora avverra una modifica". Ed e una cosa buona: vedi la mutazione direttamente nel punto di chiamata.
Mini-sintesi: quando scegliere cosa
A volte gli studenti desiderano "una sola regola per tutto". Purtroppo, o per fortuna, in programmazione succede di rado. Ma puoi tenere a mente un confronto semplice: la copia restituita rende il codice piu "funzionale" e prevedibile, mentre inout sottolinea che sta avvenendo una modifica di una variabile specifica.
| Approccio | Come appare la chiamata | Cosa si vede nel punto di chiamata | Significato tipico |
|---|---|---|---|
| Restituire una nuova copia | |
"sto creando una nuova versione" | trasformazione sicura |
|
|
"ora value cambiera" | mutazione intenzionale dello stato |
E si, a volte userai entrambi. Per esempio, dentro un metodo mutating puoi usare lo stile "crea una copia, modifica, riassegna" — ed e normale. L'importante e che il contratto esterno sia chiaro.
5. inout, mutating e accesso esclusivo
inout e un contratto di modifica, non un puntatore
A questo punto, negli studenti spesso nasce un pensiero pericoloso: "O, inout e come un riferimento/puntatore, ora posso cambiare tutto da ovunque". E Swift risponde: "Stop. Respira. inout non significa 'dammi l'indirizzo della variabile', ma un contratto di modifica esclusiva temporanea".
Detto in parole semplici, inout in Swift funziona come "apportare una modifica a un documento mentre e sulla mia scrivania, e poi restituirlo". Nei documenti sullo sviluppo di Swift viene sottolineato chiaramente che la differenza tra inout e una normale copia locale mutabile e che le modifiche vengono scritte di nuovo al codice chiamante.
C'e un altro dettaglio importante: & in Swift non e "prendi l'indirizzo come in C". Swift non promette affatto che una variabile debba avere un indirizzo stabile in memoria. In molti casi & e semplicemente un modo per dare accesso temporaneo all'interno di un'operazione, e non un vero "address-of".
Per noi, come principianti, la regola pratica e questa: pensa a inout come a "permesso concesso alla funzione di modificare la tua variabile", e non come a "ora viviamo nel mondo dei puntatori".
Metodo mutating e parametro inout: la stessa idea
Dentro uno struct hai gia visto i metodi mutating. Fuori vedi inout. In realta sono idee molto vicine: sia nell'uno sia nell'altro caso Swift richiede "accesso esclusivo" per modificare in sicurezza il valore.
Rendiamo la biblioteca un po' piu "object-like" (in senso positivo): aggiungiamo il metodo add.
import Foundation
struct Book { let id: Int; var title: String; var author: String }
struct Library {
var books: [Book] = []
mutating func add(_ book: Book) {
books.append(book)
}
}
Uso:
import Foundation
struct Book { let id: Int; var title: String; var author: String }
struct Library {
var books: [Book] = []
mutating func add(_ book: Book) { books.append(book) }
}
var library = Library()
library.add(Book(id: 1, title: "Dune", author: "Frank Herbert"))
print(library.books.count) // 1
Perche e utile? Perche le regole di modifica dello stato restano vicine ai dati. Piu avanti andrai, piu spesso sceglierai un "metodo dentro lo struct" invece di una "funzione da qualche parte fuori", semplicemente perche si legge meglio.
Perche Swift a volte segnala conflicting access
Qui molti incontrano per la prima volta messaggi del compilatore o del runtime su "conflicting access" o "exclusivity violation". Suona minaccioso, come se avessi hackerato la matrice, ma in sostanza Swift sta solo difendendo una regola: quando un valore viene modificato tramite inout/mutating, l'accesso deve essere esclusivo. Cioe mentre la modifica e in corso, nessun altro deve contemporaneamente leggere/scrivere lo stesso valore.
Nei documenti Swift questo viene spesso chiamato "law of exclusivity", e vale per inout e per i setter mutanti.
Non entreremo ora nel multithreading e nei casi complessi. Ci basta una sensazione pratica: se provi nella stessa espressione sia a leggere sia a mutare lo stesso var, Swift puo chiederti di riscrivere il codice in modo un po' piu chiaro.
Per esempio, invece di "fare tutto in una riga", spesso conviene estrarre un pezzo in una variabile temporanea, e il conflitto scompare. Non e un "rimedio di fortuna", e un modo per rendere gli accessi evidenti.
6. Collezioni dentro struct e Copy-on-Write
Qui spesso emerge un secondo livello di ansia: "Ok, Book e un valore. Ma Library contiene Array. E se l'array fosse condiviso, e le modifiche trapelassero?". La buona notizia e che, dal punto di vista dell'API, anche Array in Swift si comporta come un valore. Quindi, quando copi Library, ti aspetti ancora indipendenza.
Hai gia incontrato l'idea di Copy-on-Write (COW) nella parte sugli array: Swift cerca di non copiare la memoria prima del necessario, ma logicamente resta comunque value semantics.
Verifichiamo "con le mani", senza entrare nelle ottimizzazioni.
import Foundation
struct Library {
var tags: [String]
}
var a = Library(tags: ["sci-fi", "classic"])
var b = a
b.tags.append("epic")
print(a.tags) // ["sci-fi", "classic"]
print(b.tags) // ["sci-fi", "classic", "epic"]
All'esterno vedi esattamente cio che ti aspetti dai valori: a non e cambiato. Il fatto che dentro Swift, a un certo punto, possa aver "condiviso il buffer dell'array" per poi separarlo alla mutazione e una piacevole ottimizzazione, che non deve rompere il tuo modello mentale.
7. Esempio: la biblioteca come valore
Ora assembliamo un piccolo progetto: abbiamo una biblioteca, ne facciamo una copia, modifichiamo la copia, stampiamo entrambe. E proprio qui che value semantics smette di essere un termine e diventa comodita.
import Foundation
struct Book {
let id: Int
var title: String
var author: String
}
struct Library {
var books: [Book] = []
mutating func add(_ book: Book) {
books.append(book)
}
}
var mainLibrary = Library()
mainLibrary.add(Book(id: 1, title: "Dune", author: "Frank Herbert"))
var draftLibrary = mainLibrary
draftLibrary.add(Book(id: 2, title: "Neuromancer", author: "William Gibson"))
print(mainLibrary.books.count) // 1
print(draftLibrary.books.count) // 2
Se hai mai creato una "bozza di documento" o una "copia delle impostazioni prima di sperimentare", hai gia capito intuitivamente a cosa serve.
8. Errori tipici quando si lavora con le value semantics
Errore n. 1: aspettarsi uno "stato condiviso" dopo b = a.
Se assegni un struct a un altro, ottieni una copia indipendente del valore. Spesso il principiante si aspetta intuitivamente un comportamento del tipo "due variabili guardano lo stesso oggetto", e poi si sorprende che le modifiche non si siano "propagate". La correzione non arriva con trucchi, ma con il modello giusto: uno struct in Swift e un valore, e l'assegnazione produce una copia.
Errore n. 2: provare a "modificare l'argomento della funzione" senza inout, e poi stupirsi che fuori tutto sia rimasto uguale.
Un normale parametro di funzione e, in senso logico, un valore separato, e anche se crei una variabile locale var copy = param e la cambi, questo non uscira all'esterno. Se vuoi modificare la variabile del chiamante, usa inout e & come contratto esplicito di scrittura al ritorno. La differenza tra inout e una normale copia locale mutabile sta proprio nel comportamento di "write-back".
Errore n. 3: percepire & come "prendere l'indirizzo della variabile, ora sono quasi in C".
& in Swift non e una promessa di indirizzo stabile e non e un invito a vivere con i puntatori. E un modo per dare temporaneamente alla funzione accesso esclusivo per la modifica, e il compilatore/runtime sorvegliano con molta attenzione che non ci siano accessi in conflitto. Perfino nelle discussioni sul modello di memoria standard si sottolinea che & non e address-of, ma un meccanismo di accesso temporaneo.
Errore n. 4: cercare di infilare inout dentro una closure "per dopo".
A volte si vorrebbe fare cosi: "Ricevo un parametro inout, costruisco una closure e la eseguo piu tardi". Questo porta a situazioni molto confuse, perche inout, per sua natura, vive solo in una finestra temporale limitata. Swift limita e discute apposta questi casi, perche portavano a risultati inattesi con la "shadow copy".
Errore n. 5: trasformare inout in un martello universale "pur di farlo funzionare".
inout e uno strumento potente, ma deve riflettere l'intenzione dell'API: "questa funzione modifica il tuo argomento". Se imposti inout solo perche non vuoi restituire un nuovo valore, il codice diventa meno prevedibile: e piu difficile da leggere e testare. Spesso la copia restituita e piu semplice e piu onesta, mentre inout si lascia ai casi in cui la mutazione e davvero naturale (per esempio, i metodi che gestiscono lo stato di Library.add(...)).
GO TO FULL VERSION