Une structure Swift peut partager des objets mutables après une affectation. Pour que les copies restent indépendantes, utilisez des propriétés de type valeur ou masquez le stockage de classe derrière une copie à l’écriture. Ce guide crée un calculateur synthétique de rangée de tuiles pour un aperçu sur Mac et vérifie que la modification d’une copie laisse les autres inchangées.
Pourquoi la copie de la structure modifie-t-elle quand même l’original ?
Supposons qu’un aperçu sur ordinateur calcule la largeur d’une rangée de tuiles. Vous copiez ses réglages pour essayer une autre largeur de tuile, en vous attendant à ce que l’aperçu d’origine reste inchangé. Cette attente est déçue lorsque les deux valeurs de réglages contiennent une référence vers le même objet mutable.
Swift copie les valeurs stockées d’une structure lorsqu’on l’affecte à une autre variable. Pour une propriété de classe, cette valeur stockée est une référence : l’objet référencé reste partagé. L’enveloppe est toujours une structure, mais son comportement observable peut inclure des mutations partagées. Ces règles découlent de la distinction documentée par Swift entre types valeur et types référence.
Cet exemple complet montre le problème. Il utilise uniquement la bibliothèque standard Swift : aucune importation n’est donc nécessaire.
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
Déclarer la propriété ou l’enveloppe avec let ne gèle pas les propriétés mutables de l’objet. Cela empêche de remplacer cette référence ou cette valeur de structure. Le chapitre sur les structures et les classes de Swift distingue explicitement les références constantes de l’état immuable des objets.
Placer ces objets dans un tableau préserve également leur identité partagée. Les copies de tableaux gèrent leurs éléments indépendamment, mais un élément de type classe reste une référence. Remplacer un élément ne modifie qu’un tableau ; modifier un objet référencé par les deux tableaux est observable dans les deux. Apple illustre cette distinction dans la documentation d’Array.
Quelle conception donne des valeurs indépendantes pour le calcul ?
Définissez le contrat avant de choisir une implémentation : après var edited = original, toute modification prise en charge de l’une ou l’autre disposition doit laisser inchangée la largeur calculée de l’autre disposition. Les lecteurs du type ne devraient pas avoir à savoir si son stockage est partagé en interne.
L’aide à la décision suivante applique ce contrat à un modèle d’aperçu local. Il s’agit d’une recommandation de conception déduite des règles de copie documentées.
| Situation | Conception appropriée | Condition pour préserver l’indépendance |
|---|---|---|
| Les réglages comprennent des nombres, des chaînes ou d’autres composants à sémantique de valeur | Stocker ces valeurs directement dans la structure | Vérifier que les composants imbriqués fournissent eux aussi des valeurs observables indépendantes |
| Le stockage existant fondé sur une classe doit être conservé | Utiliser un stockage privé avec copie à l’écriture | Chaque mutation doit d’abord détacher l’état mutable partagé |
| Un instantané distinct n’est nécessaire que lors d’une opération nommée | Fournir une opération de copie explicite | Préciser que l’affectation ordinaire continue de partager les objets, sauf si l’enveloppe garantit aussi l’indépendance |
| Plusieurs parties du programme modifient intentionnellement un même objet | Conserver la sémantique de référence | Décrire dans l’API le comportement de partage de l’objet |
Pour trois réglages entiers, des propriétés de structure directes constituent le choix le plus simple. L’implémentation fondée sur une classe ci-dessous est utile pour adapter une conception de stockage existante ou comprendre le fonctionnement de cette frontière ; elle n’établit aucun avantage de performance pour ce petit calcul.
La copie à l’écriture permet à deux valeurs de partager le stockage jusqu’à ce que l’une d’elles change. Apple décrit l’encapsulation d’une classe dans une structure et l’application de la copie à l’écriture comme un moyen de combiner un stockage alloué séparément avec une sémantique de valeur dans Explorer les performances de Swift. Le simple fait d’encapsuler la classe ne met pas en œuvre ce comportement.
Une méthode copy() explicite ne suffit pas non plus : les appelants peuvent toujours utiliser une affectation ordinaire sans l’invoquer. Si l’affectation fait partie de votre contrat de valeur, les chemins de mutation de l’enveloppe doivent garantir automatiquement l’indépendance.
Comment mettre en œuvre et vérifier la copie à l’écriture ?
Utilisez un seul objet de stockage privé, créez-le à l’intérieur de l’enveloppe et n’exposez que des valeurs calculées et des méthodes de mutation contrôlées. Juste avant de modifier le stockage, appelez isKnownUniquelyReferenced. Si l’unicité n’est pas établie, remplacez le stockage par une copie indépendante. Apple documente précisément cette séquence pour mettre en œuvre la copie à l’écriture.
Le calcul de l’exemple utilise des valeurs illustratives : trois tuiles, chacune large de 40 points, avec un intervalle de 10 points entre deux tuiles adjacentes. Il y a deux intervalles, donc la largeur de la rangée est de 3 * 40 + 2 * 10 = 140 points. Porter la largeur des tuiles à 60 donne 200 points.
Pour essayer le programme complet sur un Mac, exécutez d’abord swift --version dans Terminal afin de confirmer qu’une chaîne d’outils Swift est disponible, comme le recommande le guide de démarrage de Swift. Enregistrez le code suivant dans Layout.swift, puis exécutez swift Layout.swift depuis le répertoire correspondant. Le programme effectue le calcul en mémoire et affiche ses résultats.
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
Le programme ne nécessite aucune importation ni aucun paquet externe. Les plages acceptées sont des contraintes d’exemple intentionnelles : de 1 à 1 000 colonnes, et de 0 à 1 000 points pour la largeur et l’intervalle. Elles maintiennent le calcul entier dans des limites définies. Un argument hors plage déclenche une précondition ; il n’est pas silencieusement ramené dans la plage. Si ces accesseurs acceptent plus tard du texte modifiable depuis une interface de bureau, validez cette entrée avant de les appeler et proposez à l’utilisateur un moyen de la corriger.
Les vérifications couvrent trois exigences observables : modifier la disposition copiée préserve l’original, modifier l’original préserve la copie, et conserver un instantané constant préserve son résultat lors d’une modification ultérieure. Elles testent le comportement sans exiger un nombre d’allocations particulier. Swift n’était pas disponible dans l’environnement de recherche ; cet exemple y a donc été examiné, mais pas exécuté. Le résultat affiché est celui attendu.
Qu’est-ce qui doit rester vrai à mesure que l’enveloppe évolue ?
L’indépendance de cet exemple découle de toute la frontière de son API. Le stockage est privé, aucun initialiseur n’accepte d’objet de stockage conservé ailleurs et aucun accesseur ne le renvoie. Les deux méthodes de mutation passent par la même opération de détachement. L’accesseur calcule un résultat sans modifier le stockage.
Lors de l’ajout de fonctionnalités, examinez chaque nouveau chemin susceptible de changer l’état observable. Un nouvel accesseur de modification, un indice ou une fonction de rappel d’édition doit être traité comme les méthodes existantes. Un accesseur qui renvoie un objet enfant mutable permettrait aux appelants de contourner le détachement, même si l’accesseur lui-même est en lecture seule.
La copie du stockage doit aussi couvrir tout l’état mutable pertinent pour la valeur. Ici, Storage.copy() copie trois entiers, ce qui suffit. Si le stockage contient plus tard un enfant mutable de type classe, affecter cet enfant au nouveau stockage conservera une autre référence partagée. Copiez cet enfant de manière appropriée ou remplacez-le par un composant qui fournit déjà une sémantique de valeur. Le comportement des éléments de classe documenté pour Array illustre la même frontière.
Suivez cette séquence pour étendre le calculateur :
- Copiez une disposition avant d’essayer chaque nouvelle opération de modification.
- Vérifiez que la disposition non modifiée conserve son résultat précédent.
- Recommencez en inversant les rôles.
- Conservez un instantané, effectuez plusieurs modifications ultérieures, puis vérifiez-le à nouveau.
- Examinez chaque propriété renvoyée et chaque argument d’initialiseur pour repérer les références mutables susceptibles de sortir de l’enveloppe.
Une classe de stockage final empêche les sous-classes d’ajouter un comportement, mais ne rend pas ses propriétés immuables. De même, marquer une opération mutating lui permet de modifier la structure ; c’est la logique de détachement qui préserve l’indépendance dans cet exemple.
Comment copier les objets Foundation ?
Si un modèle existant stocke des objets Foundation comme NSMutableString, utilisez le comportement de copie documenté du type concret lors du détachement du stockage. Une affectation Swift n’appelle pas automatiquement les méthodes de copie de Foundation.
Le protocole Foundation NSCopying définit la copie fonctionnelle, mais sa signification précise dépend de la classe. Sa documentation autorise également le renvoi de l’objet d’origine lorsque la classe et son contenu sont immuables. Par conséquent, l’identité de l’objet ne constitue pas à elle seule un test universel pour déterminer si une copie respecte votre contrat de valeur.
Lorsque vous avez besoin d’un résultat mutable à partir d’un type qui possède des formes distinctes, l’une mutable et l’autre immuable, consultez NSMutableCopying. Apple documente mutableCopy(with:) et la méthode pratique mutableCopy() à cette fin. Les classes sans cette distinction doivent plutôt adopter NSCopying.
Pour le calculateur de tuiles, la règle pratique est simple : un objet Foundation accepté en entrée ne doit pas rester un alias mutable externe de vos réglages censés être indépendants. Prenez un instantané indépendant approprié à l’entrée, détachez le stockage comme il convient avant les modifications et renvoyez des instantanés de valeurs plutôt que l’objet mutable interne. Avant de vous fier à son opération de copie, vérifiez comment la classe concrète traite les éventuels contenus mutables imbriqués.
La copie à l’écriture rend-elle le type Sendable ?
L’indépendance des valeurs et la conformité à la concurrence nécessitent des raisonnements distincts. La documentation de Sendable d’Apple exige que les membres stockés d’une structure Sendable vérifiée soient eux-mêmes Sendable. Une classe de stockage privée et mutable ne satisfait pas cette exigence simplement parce que le type qui l’encapsule est une structure.
La vérification d’unicité n’est pas non plus un mécanisme de synchronisation. Apple avertit que isKnownUniquelyReferenced peut renvoyer true lors d’accès simultanés et qu’une synchronisation appropriée est nécessaire dans ce cas. Cette fonction vérifie les références fortes ; des alias faibles ou non possédés peuvent toujours exister. Il est donc aussi important que le stockage reste inaccessible en dehors de l’enveloppe que de vérifier son unicité. Consultez les limites documentées de la fonction de la bibliothèque standard.
Utilisez cet exemple dans le cadre d’un seul calcul synchrone. Si une version ultérieure doit transmettre des réglages entre domaines de concurrence, la solution la plus simple consiste à utiliser une structure contenant des propriétés valeur Sendable et une conformité Sendable vérifiée. Ajouter @unchecked Sendable transfère la responsabilité de la correction à l’implémenteur ; cela ne fournit pas la coordination manquante. Apple explique ces exigences dans la référence Sendable.
Pour un petit calcul sur ordinateur, privilégiez des propriétés valeur directes dès qu’elles conviennent à la tâche. Lorsque le stockage mutable fondé sur une classe doit être conservé, préservez le contrat grâce à une propriété privée, des copies complètes du stockage, des modifications contrôlées et des vérifications de l’indépendance observable. Le résultat est prévisible : modifier les réglages de travail d’un aperçu laisse à chaque valeur de disposition conservée son propre résultat calculé.
Sources
- Types valeur et types référence en Swift — Swift.org.
- Structures et classes — Le langage de programmation Swift, projet Swift.
- Array — Documentation Apple Developer.
- Explorer les performances de Swift — Apple Developer.
- isKnownUniquelyReferenced(_:) — Documentation Apple Developer.
- Premiers pas — Swift.org.
- NSCopying — Documentation Apple Developer.
- NSMutableCopying — Documentation Apple Developer.
- Sendable — Documentation Apple Developer.