Gardez stables les propriétés utilisées pour l’égalité et le hachage tant qu’un objet sert de clé de dictionnaire. Pour un calcul Swift local sur votre Mac, choisissez l’identité de l’objet, un identifiant immuable ou un instantané de valeur selon le sens que doit avoir la recherche.
Ce guide utilise des rectangles synthétiques et de petits calculs de surface entièrement conservés en mémoire. L’objectif pratique consiste à pouvoir modifier le libellé ou les dimensions d’un rectangle sans perdre l’entrée de dictionnaire qui lui est associée, et sans confondre une recherche réussie avec un calcul à jour.
Pourquoi la modification d’un objet peut-elle casser une recherche dans un dictionnaire ?
Le Dictionary de Swift est une table de hachage dont les clés sont conformes à Hashable. Un accès par indice fondé sur une clé renvoie une valeur optionnelle, car la clé demandée peut être absente. Le hachage aide à localiser une entrée ; l’égalité détermine quelles clés sont considérées comme équivalentes. Consultez la documentation de Dictionary d’Apple.
Le contrat de Hashable relie ces opérations : des instances égales doivent fournir à Hasher les mêmes composants essentiels, dans le même ordre. Lorsque vous implémentez une clé personnalisée, utilisez les propriétés qui définissent l’égalité dans hash(into:). Apple décrit cette exigence dans Hashable.
Une clé de type référence présente une autre difficulté. Affecter une instance de classe transmet une référence à l’objet existant au lieu de créer un objet indépendant. Déclarer la référence avec let ne fige pas ses propriétés modifiables. Ces règles du langage sont documentées dans Structures and Classes.
Supposons qu’une classe Rectangle compare et hache son libellé modifiable. Vous insérez l’objet lorsque son libellé est "Tile A", puis vous le remplacez par "Tile B". La référence stockée expose alors un état différent pour les propriétés qui définissent la clé. L’emplacement initial dans le dictionnaire et les recherches ultérieures ne reposent plus sur un accord stable.
La règle de conception découle des contrats de la table de hachage et de l’égalité : l’état qui définit l’égalité de la clé doit rester stable tant qu’elle figure dans le dictionnaire. Apple énonce explicitement l’exigence correspondante pour les objets Foundation : le hachage d’un objet ne doit pas changer tant qu’il appartient à une collection qui utilise ce hachage pour le positionnement. Consultez NSObjectProtocol.hash.
Ne fondez pas un test sur l’hypothèse que toute mutation invalide renvoie immédiatement nil. Une fois le contrat de la clé rompu, une recherche qui semble réussir ne suffit pas à prouver que la conception fonctionne. Testez plutôt les règles d’égalité voulues et le comportement stable.
Quelle clé correspond à la tâche ?
Choisissez ce que représente une entrée du dictionnaire avant d’implémenter la conformité. Pour un outil de calcul de surface de rectangles, ces trois questions mènent à des conceptions différentes :
| Que doit représenter une entrée ? | Clé appropriée | Que se passe-t-il lorsque le rectangle change ? |
|---|---|---|
| Ce rectangle précis | Clé de classe fondée sur l’identité | L’entrée reste associée à cette instance |
| Un rectangle logique identifié indépendamment de l’objet actuel | Valeur d’identifiant immuable | L’entrée reste associée à l’identifiant |
| Un calcul pour des dimensions particulières | Instantané immuable des dimensions | Des dimensions modifiées produisent une clé de recherche différente |
Ce tableau est un outil de conception fondé sur les règles de Swift concernant l’identité, les valeurs et le hachage. Il distingue aussi deux obligations : conserver l’accès à une association et garder un résultat calculé à jour. Une clé d’identité stable répond à la première ; un nouveau calcul ou un instantané complet des entrées répond à la seconde.
Utiliser l’identité de l’objet lorsque l’entrée appartient à une instance
Pour une association en mémoire qui doit suivre un objet donné au fil de ses modifications, définissez l’égalité avec === et hachez son ObjectIdentifier. L’opérateur d’identité de Swift vérifie si des références pointent vers la même instance. Apple documente ObjectIdentifier comme un identifiant hachable dont les comparaisons sont valides pendant la durée de vie de l’instance. Consultez Structures and Classes et ObjectIdentifier.
Le programme synchrone ci-dessous est complet. Il nécessite un Mac disposant des outils en ligne de commande Swift ; il utilise uniquement la bibliothèque standard Swift, importée automatiquement. Enregistrez-le sous le nom IdentityKeys.swift et exécutez swift IdentityKeys.swift dans Terminal. Les exemples ont été examinés, mais n’ont pas pu être exécutés dans l’environnement de recherche, car Swift n’y était pas disponible.
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()
Le résultat attendu est Renamed lookup: 12, puis Recalculated area: 15. Les assertions expriment le contrat utile : un alias retrouve l’entrée existante, un rectangle créé séparément ne la retrouve pas, et modifier l’objet d’origine ne crée pas une autre clé.
La modification de la largeur laisse volontairement la surface stockée à 12 jusqu’à ce que le programme lui attribue 15. L’identité reste stable, mais le dictionnaire ne surveille pas les dépendances arithmétiques et ne recalcule pas les valeurs. Déterminez si votre application met à jour la valeur immédiatement après une modification ou la supprime jusqu’au prochain calcul.
L’égalité par identité est un choix qui s’applique à tout le type. Toute comparaison == sur cette classe signifie désormais « même instance ». Si le modèle définit déjà l’égalité par les dimensions ou une autre valeur, utilisez une conception de clé distincte au lieu de changer cette sémantique pour un seul dictionnaire.
Vous pouvez aussi utiliser directement ObjectIdentifier comme clé de dictionnaire, mais l’identifiant seul ne fournit pas le rectangle nécessaire aux opérations ultérieures. Gardez l’objet concerné en vie tant que son identifiant sert à effectuer des comparaisons, et supprimez les entrées obsolètes. La limite documentée de durée de vie en fait un mécanisme d’identité en mémoire à portée limitée. Consultez ObjectIdentifier.
Utiliser un instantané de valeur lorsque l’entrée représente un calcul
Pour réutiliser des résultats de surface, les dimensions elles-mêmes constituent souvent la clé appropriée. Deux rectangles mesurant 4 sur 3 peuvent partager le même résultat même s’il s’agit d’objets distincts aux libellés différents. Une modification des dimensions doit sélectionner une entrée différente.
Les structures Swift ont une sémantique de valeur, et le compilateur peut synthétiser Hashable lorsque leurs propriétés stockées sont hachables. Un instantané contenant des entiers immuables constitue donc une clé simple pour ce calcul. Consultez Structures and Classes et Hashable.
Exécutez cette autre version complète séparément avec swift SnapshotKeys.swift. Ses entrées synthétiques sont de petits entiers positifs ; l’exemple n’a donc pas besoin d’une interface générale de saisie numérique.
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()
Le résultat attendu est Original area: 12, puis Current area: 15. Les deux résultats restent accessibles, car le programme a inséré deux instantanés distincts. L’ancien résultat décrit toujours correctement les anciennes dimensions.
Incluez dans l’instantané toutes les entrées qui influent sur le calcul. Le libellé n’a aucun effet sur la surface d’un rectangle ; il est donc exclu ici. Si un calcul ultérieur dépend d’un autre paramètre, ajoutez ce paramètre à la clé. Sinon, des calculs différents risquent de partager une entrée à tort. Il s’agit d’une exigence de conception du calcul, indépendante de la conformité de la clé à Hashable.
Une structure qui se contente d’envelopper une référence de classe modifiable ne crée pas automatiquement un instantané figé de cet objet. Copiez les valeurs pertinentes dans l’instantané, comme dans cet exemple. Déterminez également quand supprimer les anciens instantanés si des modifications répétées entraînent l’accumulation de résultats devenus inutiles.
Quand faut-il plutôt utiliser un identifiant immuable ?
Utilisez un identifiant lorsque plusieurs instances d’objet doivent accéder à la même entrée logique. Pour cet outil synthétique, un identifiant entier immuable peut suffire : stockez les résultats dans [Int: Int], utilisez le let id: Int du rectangle pour y accéder, et gardez les libellés et dimensions modifiables en dehors de la clé.
Avec cette conception, deux objets ayant l’identifiant 7 accèdent intentionnellement à la même entrée du dictionnaire. Vos règles de création doivent garantir que des rectangles logiquement distincts ne reçoivent pas accidentellement le même identifiant. La mise à jour d’une clé existante remplace la valeur qui lui est associée, comme l’indique la documentation de Dictionary.
Une table de surfaces fondée sur des identifiants nécessite toujours une stratégie de mise à jour lorsque les dimensions changent. Si vous souhaitez conserver les résultats pour chaque ensemble d’entrées distinct, utilisez des instantanés. Si vous ne voulez que le résultat le plus récent pour chaque rectangle logique, un identifiant accompagné d’un recalcul explicite convient mieux.
Les dictionnaires Foundation ou les acteurs résolvent-ils le problème des clés modifiables ?
Foundation applique un autre contrat à l’insertion. NSMutableDictionary.setObject(_:forKey:) exige une clé NSCopying et la copie lors de l’insertion. Une copie de clé peut constituer un instantané si son implémentation garantit un état de clé stable. Les recherches ultérieures doivent correspondre à cette copie stockée. Consultez la méthode Apple setObject(_:forKey:).
Ne déduisez pas de ce comportement que le dictionnaire natif de Swift crée une copie indépendante d’une instance de classe utilisée comme clé. L’affectation d’une classe en Swift repose sur la sémantique de référence. Pour les clés Foundation personnalisées, vérifiez ensemble le comportement de copie, d’égalité et de hachage ; l’exigence de hachage de Foundation s’applique toujours.
La concurrence est une question distincte. Les acteurs coordonnent l’accès à leur état mutable isolé, et Sendable décrit les valeurs pouvant traverser des domaines de concurrence conformément à ses exigences. Ni l’un ni l’autre ne change la signification de l’égalité d’une clé ni ne répare une clé dont l’état définissant a changé après l’insertion. Consultez le chapitre sur la concurrence de Swift et la documentation de Sendable d’Apple.
L’exemple d’instantané déclare explicitement Sendable, car ses entiers stockés satisfont aux exigences du protocole pour les types valeur. Si vous déplacez ensuite les calculs de surface dans un acteur, transmettez un instantané complet et laissez l’acteur gérer son dictionnaire de résultats. Le rectangle modifiable peut rester dans son domaine d’isolation actuel. Ces exemples synchrones n’ont pas besoin d’un acteur.
Vérifier le contrat avant de se fier à une recherche
Validez la conception choisie à l’aide d’une séquence courte et déterministe :
- Insérez une entrée et vérifiez sa recherche initiale.
- Modifiez une propriété exclue de l’égalité et du hachage ; vérifiez que la même entrée reste accessible.
- Comparez un alias à un objet ou à un instantané créé séparément ; vérifiez que les résultats correspondent au sens d’égalité recherché.
- Modifiez une entrée du calcul ; vérifiez que le résultat est actualisé, supprimé ou accessible au moyen d’un nouvel instantané.
- Vérifiez le nombre d’entrées du dictionnaire afin de confirmer que le remplacement et l’insertion se comportent comme prévu.
Si une implémentation existante modifie l’état qui définit la clé, corrigez le modèle de clé avant de vous fier à d’autres recherches. Pour ce petit calcul local, reconstruire les résultats à partir d’une liste distincte d’entrées synthétiques est plus clair que de considérer les opérations ultérieures du dictionnaire comme un mécanisme de réparation. Conservez suffisamment d’entrées d’origine pour pouvoir recalculer, plutôt que de supprimer silencieusement une entrée introuvable.
Choisissez l’identité pour une association liée à un objet vivant, un identifiant immuable pour un élément logique et un instantané de valeur pour un calcul. Définissez ensuite explicitement la stratégie de mise à jour des résultats. Ces choix permettent aux recherches de rester pertinentes lorsque le rectangle modifiable change.
Sources
- Hashable — Apple
- Dictionary — Apple
- Structures and Classes — Swift.org
- ObjectIdentifier — Apple
- hash — Apple
- setObject(_:forKey:) — Apple
- Concurrency — Swift.org
- Sendable — Apple