Mantieni stabili le proprietà usate per l’uguaglianza e l’hashing finché un oggetto è una chiave del dizionario. Per un calcolo Swift locale sul Mac, scegli l’identità dell’oggetto, un ID immutabile o uno snapshot dei valori in base al significato che dovrebbe avere la ricerca.
Questa guida usa rettangoli sintetici e semplici calcoli dell’area, mantenuti interamente in memoria. L’obiettivo pratico è permettere di cambiare l’etichetta o le dimensioni di un rettangolo senza perdere la voce del dizionario associata e senza confondere una ricerca riuscita con un calcolo aggiornato.
Perché modificare un oggetto può interrompere una ricerca nel dizionario?
Il Dictionary di Swift è una tabella hash le cui chiavi sono conformi a Hashable. Un accesso tramite pedice basato su una chiave restituisce un valore opzionale, perché la chiave richiesta potrebbe essere assente. L’hashing aiuta a trovare una voce; l’uguaglianza determina quali chiavi sono considerate equivalenti. Consulta la documentazione di Dictionary di Apple.
Il contratto di Hashable collega queste operazioni: le istanze uguali devono fornire a Hasher gli stessi componenti essenziali, nello stesso ordine. Quando implementi una chiave personalizzata, usa le proprietà che definiscono l’uguaglianza in hash(into:). Apple descrive questo requisito in Hashable.
Una chiave di tipo riferimento comporta un’ulteriore considerazione. Assegnare un’istanza di classe passa un riferimento all’oggetto esistente, anziché creare un oggetto indipendente. Dichiarare il riferimento con let non congela le sue proprietà mutabili. Sono regole del linguaggio documentate in Strutture e classi.
Supponiamo che una classe Rectangle confronti e sottoponga a hashing la propria etichetta modificabile. Inserisci l’oggetto quando la sua etichetta è \"Tile A\", poi cambiala in \"Tile B\". Il riferimento memorizzato ora espone uno stato diverso per le proprietà che definiscono la chiave. Il posizionamento originale nel dizionario e le ricerche successive non hanno più un accordo stabile.
La regola di progettazione deriva dai contratti della tabella hash e dell’uguaglianza: mantieni stabile lo stato che definisce l’uguaglianza della chiave per tutta la permanenza nel dizionario. Apple enuncia esplicitamente il requisito corrispondente per gli oggetti Foundation: l’hash di un oggetto non deve cambiare mentre appartiene a una raccolta che lo usa per determinarne il posizionamento. Consulta NSObjectProtocol.hash.
Non basare un test sull’ipotesi che ogni modifica non valida restituisca immediatamente nil. Una volta violato il contratto della chiave, una ricerca che sembra riuscire non basta a dimostrare che la progettazione funziona. Verifica invece le regole di uguaglianza previste e il comportamento stabile.
Quale chiave corrisponde al compito?
Scegli il significato di una voce del dizionario prima di scrivere una conformità. Per uno strumento che calcola l’area dei rettangoli, queste tre domande portano a progettazioni diverse:
| Che cosa dovrebbe rappresentare una voce? | Chiave adatta | Che cosa succede quando il rettangolo cambia? |
|---|---|---|
| Questo specifico oggetto rettangolo | Chiave di classe basata sull’identità | La voce resta associata a quell’istanza |
| Un rettangolo logico identificato indipendentemente dall’oggetto attuale | Valore ID immutabile | La voce resta associata all’ID |
| Un calcolo relativo a dimensioni specifiche | Snapshot immutabile delle dimensioni | Dimensioni diverse producono una chiave di ricerca diversa |
Questa tabella è uno strumento di progettazione derivato dalle regole di Swift su identità, valori e hashing. Separa anche due obblighi: mantenere raggiungibile un’associazione e mantenere aggiornato un risultato calcolato. Una chiave d’identità stabile risolve il primo; il ricalcolo o uno snapshot completo degli input risolve il secondo.
Usa l’identità dell’oggetto quando la voce appartiene a una singola istanza
Per un’associazione in memoria che dovrebbe seguire un oggetto durante le modifiche, definisci l’uguaglianza con === e applica l’hashing al suo ObjectIdentifier. L’operatore di identità di Swift verifica se i riferimenti puntano alla stessa istanza. Apple documenta ObjectIdentifier come identificatore hashable i cui confronti sono validi per tutta la durata dell’istanza. Consulta Strutture e classi e ObjectIdentifier.
Quello che segue è un programma sincrono completo. Richiede un Mac con gli strumenti a riga di comando di Swift disponibili e usa soltanto la libreria standard di Swift, importata automaticamente. Salvalo come IdentityKeys.swift ed esegui swift IdentityKeys.swift nel Terminale. Gli esempi sono stati esaminati, ma non è stato possibile eseguirli nell’ambiente di ricerca perché Swift non era disponibile.
final class Rectangle: Hashable {
var label: String
var width: Int
var height: Int
init(label: String, width: Int, height: Int) {
self.label = label
self.width = width
self.height = height
}
static func == (lhs: Rectangle, rhs: Rectangle) -> Bool {
lhs === rhs
}
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}
func demonstrateIdentityKeys() {
let rectangle = Rectangle(label: "Tile A", width: 4, height: 3)
let alias = rectangle
let separate = Rectangle(label: "Tile A", width: 4, height: 3)
var areas: [Rectangle: Int] = [
rectangle: rectangle.width * rectangle.height
]
rectangle.label = "Tile B"
assert(areas[alias] == 12)
assert(areas[separate] == nil)
rectangle.width = 5
assert(areas[rectangle] == 12) // Still the previous calculation.
areas[rectangle] = rectangle.width * rectangle.height
assert(areas[rectangle] == 15)
assert(areas.count == 1)
print("Renamed lookup: 12")
print("Recalculated area: \(areas[rectangle]!)")
}
demonstrateIdentityKeys()
L’output previsto è Renamed lookup: 12, seguito da Recalculated area: 15. Le asserzioni esprimono il contratto utile: un alias trova la voce esistente, un rettangolo creato separatamente non la trova e modificare l’oggetto originale non crea un’altra chiave.
La modifica della larghezza lascia intenzionalmente l’area memorizzata a 12 finché il programma non assegna 15. L’identità resta stabile, ma il dizionario non rileva le dipendenze aritmetiche e non ricalcola i valori. Decidi se l’applicazione aggiorna il valore subito dopo una modifica o lo rimuove fino al calcolo successivo.
L’uguaglianza basata sull’identità è una scelta che vale per l’intero tipo. Ogni confronto == su questa classe ora significa «la stessa istanza». Se il modello definisce già l’uguaglianza in base alle dimensioni o a un altro valore, usa una progettazione di chiave separata anziché cambiare quel significato solo per un dizionario.
Puoi anche usare direttamente ObjectIdentifier come chiave di un dizionario, ma l’identificatore da solo non fornisce il rettangolo necessario per le operazioni successive. Mantieni vivo l’oggetto pertinente finché il suo identificatore viene usato per i confronti e rimuovi le voci obsolete. Il limite di durata documentato rende questo un meccanismo d’identità circoscritto alla memoria. Consulta ObjectIdentifier.
Usa uno snapshot dei valori quando la voce rappresenta un calcolo
Per risultati dell’area riutilizzabili, spesso è opportuno usare come chiave le dimensioni stesse. Due rettangoli che misurano 4 per 3 possono condividere lo stesso risultato anche se sono oggetti separati con etichette diverse. Modificare le dimensioni dovrebbe selezionare una voce diversa.
Le strutture Swift hanno semantica di valore e il compilatore può sintetizzare Hashable quando le loro proprietà memorizzate sono hashable. Uno snapshot contenente interi immutabili fornisce quindi una chiave semplice per questo calcolo. Consulta Strutture e classi e Hashable.
Esegui separatamente questa alternativa completa con swift SnapshotKeys.swift. Gli input sintetici sono piccoli interi positivi, quindi l’esempio non ha bisogno di un’interfaccia generica per l’input numerico.
final class EditableRectangle {
var label: String
var width: Int
var height: Int
init(label: String, width: Int, height: Int) {
self.label = label
self.width = width
self.height = height
}
}
struct AreaInputs: Hashable, Sendable {
let width: Int
let height: Int
}
func demonstrateSnapshotKeys() {
let rectangle = EditableRectangle(
label: "Tile A", width: 4, height: 3
)
var areas: [AreaInputs: Int] = [:]
let original = AreaInputs(
width: rectangle.width, height: rectangle.height
)
areas[original] = original.width * original.height
rectangle.label = "Tile B"
rectangle.width = 5
let current = AreaInputs(
width: rectangle.width, height: rectangle.height
)
assert(areas[original] == 12)
assert(areas[current] == nil)
areas[current] = current.width * current.height
assert(areas[current] == 15)
assert(areas.count == 2)
print("Original area: \(areas[original]!)")
print("Current area: \(areas[current]!)")
}
demonstrateSnapshotKeys()
L’output previsto è prima Original area: 12, poi Current area: 15. Entrambi i risultati restano raggiungibili perché il programma ha inserito due snapshot distinti. Il vecchio risultato descrive ancora correttamente le dimensioni precedenti.
Includi nello snapshot ogni input che influisce sul calcolo. Un’etichetta è irrilevante per l’area di un rettangolo e qui viene quindi esclusa. Se un calcolo successivo dipende da un’altra impostazione, aggiungi anche quella alla chiave. Altrimenti, calcoli diversi potrebbero condividere erroneamente una voce. È un requisito di progettazione del calcolo, indipendente dalla conformità della chiave a Hashable.
Una struttura che si limita a contenere un riferimento a una classe mutabile non crea automaticamente uno snapshot congelato di quell’oggetto. Copia i valori pertinenti nello snapshot, come fa questo esempio. Decidi anche quando eliminare i vecchi snapshot, nel caso in cui le modifiche ripetute accumulino risultati non più necessari.
Quando dovresti usare invece un ID immutabile?
Usa un ID quando più istanze di un oggetto dovrebbero accedere alla stessa voce logica. Per questo strumento sintetico può bastare un ID intero immutabile: memorizza i risultati in [Int: Int], usa let id: Int di un rettangolo per l’accesso e lascia le etichette e le dimensioni modificabili fuori dalla chiave.
Con questa progettazione, due oggetti con ID 7 accedono intenzionalmente alla stessa voce del dizionario. Le regole di creazione devono garantire che rettangoli logici diversi non ricevano accidentalmente lo stesso ID. Come documentato in Dictionary, l’aggiornamento di una chiave esistente sostituisce il valore associato.
Una tabella delle aree basata su ID richiede comunque una regola di aggiornamento quando cambiano le dimensioni. Se vuoi conservare i risultati per ogni insieme distinto di input, usa gli snapshot. Se vuoi conservare soltanto il risultato più recente per ciascun rettangolo logico, un ID con ricalcolo esplicito è più adatto.
I dizionari Foundation o gli actor risolvono il problema delle chiavi mutabili?
Foundation ha un contratto d’inserimento diverso. NSMutableDictionary.setObject(_:forKey:) richiede che la chiave sia conforme a NSCopying e la copia durante l’inserimento. Una copia può creare uno snapshot quando la relativa implementazione di copia fornisce uno stato stabile per la chiave. Le ricerche successive devono corrispondere a quella copia memorizzata. Consulta il metodo Apple setObject(_:forKey:).
Non dedurre da questo comportamento che il dizionario nativo di Swift crei una copia indipendente di un’istanza di classe usata come chiave. L’assegnazione delle classi Swift ha semantica di riferimento. Per le chiavi personalizzate di Foundation, verifica insieme il comportamento di copia, uguaglianza e hashing; si applica ancora il requisito sull’hash di Foundation.
La concorrenza è una questione separata. Gli actor coordinano l’accesso al proprio stato mutabile isolato e Sendable descrive i valori che possono attraversare domini di concorrenza nel rispetto dei suoi requisiti. Nessuno dei due cambia il significato dell’uguaglianza di una chiave o corregge una chiave il cui stato determinante cambia dopo l’inserimento. Consulta il capitolo sulla concorrenza di Swift e la documentazione di Sendable di Apple.
L’esempio dello snapshot dichiara esplicitamente Sendable perché i suoi interi memorizzati soddisfano i requisiti del protocollo per i tipi valore. Se in seguito sposti i calcoli dell’area in un actor, invia uno snapshot completo e lascia che l’actor gestisca il dizionario dei risultati. Il rettangolo mutabile può restare nel suo attuale dominio d’isolamento. Questi esempi sincroni non richiedono un actor.
Verifica il contratto prima di affidarti a una ricerca
Usa una sequenza piccola e deterministica per convalidare la progettazione scelta:
- Inserisci una voce e conferma che la ricerca iniziale riesca.
- Modifica una proprietà esclusa da uguaglianza e hashing; conferma che la stessa voce resti raggiungibile.
- Confronta un alias con un oggetto o uno snapshot creato separatamente; verifica che i risultati corrispondano al significato previsto dell’uguaglianza.
- Modifica un input del calcolo; conferma che il risultato venga aggiornato, rimosso o recuperato tramite un nuovo snapshot.
- Controlla il numero di elementi del dizionario per verificare che sostituzioni e inserimenti si comportino come previsto.
Se un’implementazione esistente modifica lo stato che definisce la chiave, correggi il modello della chiave prima di affidarti ad altre ricerche. Per questo piccolo calcolo locale è più chiaro ricostruire i risultati da un elenco separato di input sintetici che trattare le operazioni successive sul dizionario come un meccanismo di riparazione. Conserva abbastanza input originale da poter ricalcolare, anziché eliminare silenziosamente una voce che non riesci a trovare.
Scegli l’identità per un’associazione con un singolo oggetto attivo, un ID immutabile per un elemento logico e uno snapshot dei valori per un calcolo. Poi rendi esplicita la regola di aggiornamento dei risultati. Insieme, queste decisioni mantengono significative le ricerche mentre il rettangolo modificabile cambia.
Fonti
- Hashable — Apple
- Dictionary — Apple
- Strutture e classi — Swift.org
- ObjectIdentifier — Apple
- hash — Apple
- setObject(_:forKey:) — Apple
- Concorrenza — Swift.org
- Sendable — Apple