Una struct Swift può condividere oggetti mutabili dopo un’assegnazione. Per mantenere indipendenti le copie, usa proprietà di tipi valore oppure nascondi l’archiviazione in una classe dietro un meccanismo copy-on-write. Questa guida crea un calcolatore sintetico per la larghezza di una fila di riquadri, destinato a un’anteprima desktop per Mac, e verifica che modificare una copia lasci invariate le altre.
Perché copiare la struct modifica comunque l’originale?
Supponiamo che un’anteprima desktop calcoli la larghezza di una fila di riquadri. Ne copi le impostazioni per provare un’altra larghezza dei riquadri, aspettandoti che l’anteprima originale resti invariata. Questa aspettativa viene delusa quando entrambi i valori delle impostazioni contengono un riferimento allo stesso oggetto mutabile.
Swift copia i valori memorizzati in una struct quando li assegna a un’altra variabile. Per una proprietà di classe, il valore memorizzato è un riferimento: l’oggetto referenziato resta condiviso. Il contenitore è comunque una struct, ma il suo comportamento osservabile può includere modifiche condivise. Queste regole derivano dalla distinzione documentata da Swift tra tipi valore e tipi riferimento.
Questo esempio completo illustra il problema. Usa solo la libreria standard di Swift, quindi non occorre importare nulla.
final class TileDimensions {
var width: Int
init(width: Int) {
self.width = width
}
}
struct SharedLayout {
let dimensions: TileDimensions
}
let original = SharedLayout(
dimensions: TileDimensions(width: 40)
)
let edited = original
edited.dimensions.width = 60
assert(original.dimensions === edited.dimensions)
assert(original.dimensions.width == 60)
print(original.dimensions.width) // 60
Dichiarare la proprietà o il contenitore con let non rende immutabili le proprietà mutabili dell’oggetto. Impedisce di sostituire quel riferimento o valore di struct. Il capitolo sulle strutture e le classi di Swift distingue esplicitamente i riferimenti costanti dallo stato immutabile degli oggetti.
Anche inserire questi oggetti in un array mantiene la loro identità condivisa. Le copie degli array gestiscono gli elementi in modo indipendente, ma un elemento di tipo classe è a sua volta un riferimento. Sostituire un elemento modifica un solo array; modificare un oggetto referenziato da entrambi gli array è osservabile attraverso entrambi. Apple illustra questa distinzione nella documentazione di Array.
Quale progetto offre valori indipendenti per il calcolo?
Definisci il contratto prima di scegliere un’implementazione: dopo var edited = original, qualsiasi modifica supportata a uno dei due layout deve lasciare invariata la larghezza calcolata dell’altro layout. Chi usa il tipo non dovrebbe dover sapere se la sua archiviazione è condivisa internamente.
La seguente guida decisionale applica questo contratto a un modello locale per l’anteprima. È una raccomandazione progettuale derivata dalle regole di copia documentate.
| Situazione | Progetto adatto | Condizione per preservare l’indipendenza |
|---|---|---|
| Le impostazioni consistono in numeri, stringhe o altri componenti con semantica di valore | Memorizza direttamente quei valori nella struct | Verifica che anche i componenti annidati forniscano valori osservabili indipendenti |
| È necessario mantenere l’archiviazione esistente basata su una classe | Usa un’archiviazione privata con copy-on-write | Ogni modifica deve prima separare lo stato mutabile condiviso |
| Serve uno snapshot separato solo in un’operazione specifica | Fornisci un’operazione di copia esplicita | Documenta che la normale assegnazione continua a condividere gli oggetti, a meno che il contenitore non imponga anche l’indipendenza |
| Più parti del programma modificano intenzionalmente un unico oggetto | Mantieni la semantica di riferimento | Descrivi nell’API il comportamento dell’oggetto condiviso |
Per tre impostazioni intere, le proprietà dirette della struct sono la scelta più semplice. L’implementazione basata su una classe qui sotto è utile per adattare un progetto di archiviazione esistente o capire come funziona il confine; non dimostra un vantaggio prestazionale per questo piccolo calcolo.
Il copy-on-write consente a due valori di condividere l’archiviazione finché uno dei due non cambia. Apple descrive l’incapsulamento di una classe in una struct e l’applicazione del copy-on-write come un modo per combinare un’archiviazione allocata separatamente con la semantica di valore, in Explore Swift performance. Limitarsi a racchiudere la classe in un contenitore non implementa questo comportamento.
Anche un metodo copy() esplicito, da solo, lascia una lacuna: i chiamanti possono comunque usare la normale assegnazione senza invocarlo. Se l’assegnazione fa parte del contratto di valore, i percorsi di modifica del contenitore devono garantire automaticamente l’indipendenza.
Come implementare e verificare il copy-on-write?
Usa un unico oggetto di archiviazione privato, crealo all’interno del contenitore ed esponi solo valori calcolati e metodi di modifica controllati. Subito prima di modificare l’archiviazione, chiama isKnownUniquelyReferenced. Se non è possibile stabilirne l’unicità, sostituisci l’archiviazione con una copia indipendente. Apple documenta questa sequenza in modo specifico per implementare il copy-on-write.
Il calcolo dell’esempio usa dati illustrativi: tre riquadri larghi 40 punti ciascuno, con uno spazio di 10 punti tra riquadri adiacenti. Ci sono due spazi, quindi la larghezza della fila è di 3 * 40 + 2 * 10 = 140 punti. Aumentando la larghezza dei riquadri a 60, si ottengono 200 punti.
Per provare il programma completo su un Mac, esegui prima swift --version nel Terminale per verificare che sia disponibile una toolchain Swift, come raccomandato dalla guida introduttiva di Swift. Salva il codice seguente come Layout.swift, quindi esegui swift Layout.swift dalla relativa directory. Il programma esegue il calcolo in memoria e ne stampa i risultati.
struct TileLayout {
private final class Storage {
let columns: Int
var tileWidth: Int
var gap: Int
init(columns: Int, tileWidth: Int, gap: Int) {
self.columns = columns
self.tileWidth = tileWidth
self.gap = gap
}
func copy() -> Storage {
Storage(
columns: columns,
tileWidth: tileWidth,
gap: gap
)
}
}
private var storage: Storage
init(columns: Int, tileWidth: Int, gap: Int) {
precondition((1...1000).contains(columns))
precondition((0...1000).contains(tileWidth))
precondition((0...1000).contains(gap))
storage = Storage(
columns: columns,
tileWidth: tileWidth,
gap: gap
)
}
var rowWidth: Int {
storage.columns * storage.tileWidth
+ (storage.columns - 1) * storage.gap
}
private mutating func ensureUniqueStorage() {
if !isKnownUniquelyReferenced(&storage) {
storage = storage.copy()
}
}
mutating func setTileWidth(_ width: Int) {
precondition((0...1000).contains(width))
ensureUniqueStorage()
storage.tileWidth = width
}
mutating func setGap(_ gap: Int) {
precondition((0...1000).contains(gap))
ensureUniqueStorage()
storage.gap = gap
}
}
var original = TileLayout(columns: 3, tileWidth: 40, gap: 10)
var edited = original
edited.setTileWidth(60)
assert(original.rowWidth == 140)
assert(edited.rowWidth == 200)
original.setGap(5)
assert(original.rowWidth == 130)
assert(edited.rowWidth == 200)
let snapshot = edited
edited.setGap(20)
assert(snapshot.rowWidth == 200)
assert(edited.rowWidth == 220)
print(original.rowWidth, snapshot.rowWidth, edited.rowWidth)
// Expected output: 130 200 220
Il programma non richiede importazioni né pacchetti esterni. Gli intervalli accettati sono vincoli intenzionali dell’esempio: da una a 1.000 colonne e da zero a 1.000 punti per larghezza e spazio. Mantengono entro limiti l’operazione sugli interi. Un argomento fuori intervallo viola una precondizione; il valore non viene limitato automaticamente. Se in seguito questi setter accetteranno testo modificabile da un’interfaccia desktop, convalida l’input prima di chiamarli e consenti all’utente di correggerlo.
I controlli coprono tre requisiti osservabili: modificare il layout copiato preserva l’originale, modificare l’originale preserva la copia e mantenere uno snapshot costante ne preserva il risultato durante una modifica successiva. Verificano il comportamento senza richiedere un particolare numero di allocazioni. Swift non era disponibile nell’ambiente di ricerca, quindi l’esempio è stato esaminato ma non eseguito; il risultato mostrato è quello previsto.
Cosa deve rimanere vero quando il contenitore si evolve?
L’indipendenza dell’esempio deriva dall’intero confine dell’API. L’archiviazione è privata, nessun inizializzatore accetta un oggetto di archiviazione mantenuto altrove e nessun getter lo restituisce. Entrambi i metodi di modifica passano attraverso la stessa operazione di separazione. Il getter calcola un risultato senza modificare l’archiviazione.
Quando aggiungi funzionalità, esamina ogni nuovo percorso che può cambiare lo stato osservabile. Un nuovo setter, pedice o callback di modifica richiede lo stesso trattamento dei metodi esistenti. Un getter che restituisce un oggetto figlio mutabile permetterebbe ai chiamanti di aggirare la separazione, anche se il getter stesso è di sola lettura.
Anche la copia dell’archiviazione deve includere tutto lo stato mutabile rilevante per il valore. Qui, Storage.copy() copia tre interi, ed è sufficiente. Se in seguito l’archiviazione conterrà un oggetto figlio mutabile di tipo classe, assegnarlo alla nuova archiviazione manterrà un altro riferimento condiviso. Copia adeguatamente quell’oggetto figlio oppure sostituiscilo con un componente che offra già semantica di valore. Il comportamento degli elementi di classe documentato per Array illustra lo stesso confine.
Segui questa sequenza quando estendi il calcolatore:
- Copia un layout prima di provare ogni nuova operazione di modifica.
- Verifica che il layout non modificato conservi il risultato precedente.
- Ripeti invertendo i ruoli.
- Mantieni uno snapshot, esegui diverse modifiche successive e controllalo di nuovo.
- Esamina ogni proprietà restituita e argomento dell’inizializzatore per verificare che non possano uscire dal contenitore riferimenti mutabili.
Una classe di archiviazione final impedisce alle sottoclassi di aggiungere comportamento, ma non rende immutabili le sue proprietà. Allo stesso modo, contrassegnare un’operazione come mutating consente di aggiornare la struct; è la logica di separazione a preservare l’indipendenza di questo esempio.
Come si copiano gli oggetti Foundation?
Se un modello esistente memorizza oggetti Foundation come NSMutableString, usa il comportamento di copia documentato del tipo concreto quando implementi la separazione dell’archiviazione. L’assegnazione Swift non chiama automaticamente i metodi di copia di Foundation.
Il protocollo Foundation NSCopying definisce la copia funzionale, mentre il significato preciso dipende dalla classe. La sua documentazione consente anche di restituire l’oggetto originale quando sia la classe sia i suoi contenuti sono immutabili. Di conseguenza, l’identità dell’oggetto da sola non è un criterio universale per stabilire se una copia soddisfa il contratto di valore.
Quando ti serve un risultato mutabile da un tipo che offre forme mutabili e immutabili distinte, consulta NSMutableCopying. Apple documenta a tale scopo mutableCopy(with:) e il metodo di convenienza mutableCopy(). Le classi prive di questa distinzione dovrebbero adottare NSCopying.
Per il calcolatore di riquadri, il confine pratico è semplice: un oggetto Foundation accettato come input non deve restare un alias mutabile esterno alle impostazioni che dovrebbero essere indipendenti. Crea uno snapshot indipendente adatto all’ingresso, separa opportunamente l’archiviazione prima delle modifiche e restituisci snapshot di valori invece dell’oggetto mutabile interno. Prima di affidarti alla sua operazione di copia, verifica come la classe concreta tratta eventuali contenuti mutabili annidati.
Il copy-on-write rende il tipo Sendable?
L’indipendenza dei valori e la conformità alla concorrenza richiedono valutazioni separate. La documentazione di Apple su Sendable richiede che i membri memorizzati di una struct Sendable verificata siano a loro volta Sendable. Una classe di archiviazione mutabile privata non soddisfa questo requisito solo perché il tipo che la contiene è una struct.
Anche il controllo di unicità non è una sincronizzazione. Apple avverte che isKnownUniquelyReferenced può restituire true durante un accesso simultaneo e richiede una sincronizzazione appropriata in questi casi. Controlla i riferimenti forti; possono comunque esistere alias deboli o non posseduti. Perciò è importante anche mantenere l’archiviazione inaccessibile al di fuori del contenitore, oltre a verificarne l’unicità. Consulta i limiti documentati della funzione della libreria standard.
Mantieni questo esempio in un unico contesto di calcolo sincrono. Se una versione successiva dovrà passare impostazioni tra domini di concorrenza, l’alternativa più semplice è una struct con proprietà valore Sendable e conformità Sendable verificata. Aggiungere @unchecked Sendable trasferisce la responsabilità della correttezza a chi implementa il tipo; non fornisce la sincronizzazione mancante. Apple spiega questi requisiti nel riferimento a Sendable.
Per un piccolo calcolo desktop, preferisci proprietà valore dirette quando sono sufficienti. Quando è necessario mantenere l’archiviazione in una classe mutabile, preserva il contratto con proprietà privata, copie complete dell’archiviazione, modifiche controllate e verifiche dell’indipendenza osservabile. Il risultato è prevedibile: modificare le impostazioni di lavoro di un’anteprima lascia a ogni valore di layout conservato il proprio risultato calcolato.
Fonti
- Tipi valore e tipi riferimento in Swift — Swift.org.
- Strutture e classi — The Swift Programming Language, progetto Swift.
- Array — Documentazione per sviluppatori Apple.
- Explore Swift performance — Apple Developer.
- isKnownUniquelyReferenced(_:) — Documentazione per sviluppatori Apple.
- Introduzione — Swift.org.
- NSCopying — Documentazione per sviluppatori Apple.
- NSMutableCopying — Documentazione per sviluppatori Apple.
- Sendable — Documentazione per sviluppatori Apple.