Copie profonde contre copie superficielle - et comment les utiliser dans Swift

La copie d'un objet a toujours été une partie essentielle du paradigme de codage. Que ce soit en Swift, Objective-C, JAVA ou n’importe quel autre langage, nous aurons toujours besoin de copier un objet pour l’utiliser dans différents contextes.

Dans cet article, nous verrons en détail comment copier différents types de données dans Swift et comment ils se comportent dans différentes circonstances.

Types de valeur et de référence

Tous les types de données dans Swift se divisent en gros en deux catégories, à savoir les types de valeur et les types de référence.

  • Type de valeur - chaque instance conserve une copie unique de ses données. Les types de données entrant dans cette catégorie comprennent - tous les types de données de base, struct, enum, array, tuples.
  • Type de référence - Les instances partagent une seule copie des données et le type est généralement défini en tant que classe.

La caractéristique la plus distinctive des deux types réside dans leur comportement de copie.

Qu'est-ce qu'une copie profonde et superficielle?

Une instance, qu’il s’agisse d’un type de valeur ou d’un type de référence, peut être copiée de l’une des manières suivantes:

Copie profonde - Tout dupliquer

  • Avec une copie profonde, tout objet pointé par la source est copié et la copie est pointée par la destination. Donc, deux objets complètement séparés seront créés.
  • Collections - Une copie intégrale d'une collection est constituée de deux collections avec tous les éléments de la collection d'origine dupliqués.
  • Moins sujet aux conditions de concurrence et fonctionne bien dans un environnement multithread - les modifications apportées à un objet n'auront aucun effet sur un autre objet.
  • Les types de valeur sont copiés profondément.

Dans le code ci-dessus,

  • Ligne 1: arr1 - tableau (un type de valeur) de chaînes
  • Ligne 2: arr1 est assigné à arr2. Cela créera une copie détaillée de arr1, puis assignera cette copie à arr2
  • Lignes 7 à 11: les modifications effectuées dans arr2 ne sont pas reflétées dans arr1.

C'est ce que la copie profonde est - des instances complètement séparées. Le même concept fonctionne avec tous les types de valeur.

Dans certains scénarios, c'est-à-dire lorsqu'un type de valeur contient des types de référence imbriqués, la copie détaillée révèle un type de comportement différent. Nous verrons cela dans les prochaines sections.

Copie superficielle - duplique le moins possible

  • Avec une copie superficielle, tout objet pointé par la source est également pointé par la destination. Donc, un seul objet sera créé dans la mémoire.
  • Collections - Une copie superficielle d'une collection est une copie de la structure de la collection, pas des éléments. Avec une copie superficielle, deux collections partagent désormais les éléments individuels.
  • Plus rapide - seule la référence est copiée.
  • La copie des types de référence crée une copie superficielle.

Dans le code ci-dessus,

  • Lignes 1 à 8: type de classe d'adresse
  • Ligne 10: a1 - une instance de type adresse
  • Ligne 11: a1 est attribué à a2. Cela créera une copie superficielle de a1, puis assignera cette copie à a2, c'est-à-dire que seule la référence est copiée dans a2.
  • Lignes 16 à 19: tout changement effectué dans a2 sera certainement reflété dans a1.

Dans l'illustration ci-dessus, nous pouvons voir que a1 et a2 désignent tous deux la même adresse mémoire.

Copier profondément les types de référence

A présent, nous savons que chaque fois que nous essayons de copier un type de référence, seule la référence à l'objet est copiée. Aucun nouvel objet n'est créé. Et si nous voulons créer un objet complètement séparé?

Nous pouvons créer une copie complète du type de référence en utilisant la méthode copy (). Selon la documentation,

copy () - Retourne l'objet retourné par copy (avec :).

C'est une méthode pratique pour les classes qui adoptent le protocole NSCopying. Une exception est générée s'il n'y a pas d'implémentation pour copy (with :).

Restaurez la classe d’adresses créée dans le fragment de code 2 afin de vous conformer au protocole NSCopying.

Dans le code ci-dessus,

  • Lignes 1 à 14: Le type de classe d’adresse est conforme à NSCopying et implémente la méthode copy (with :)
  • Ligne 16: a1 - une instance de type adresse
  • Ligne 17: a1 est assigné à a2 en utilisant la méthode copy (). Cela créera une copie complète de a1, puis assignera cette copie à a2, c'est-à-dire qu'un nouvel objet sera créé.
  • Lignes 22 à 25: toute modification effectuée dans a2 ne sera pas reflétée dans a1.

Comme le montre l'illustration ci-dessus, a1 et a2 pointent vers des emplacements de mémoire différents.

Regardons un autre exemple. Cette fois, nous verrons comment cela fonctionne avec les types de référence imbriqués - un type de référence contenant un autre type de référence.

Dans le code ci-dessus,

  • Ligne 22: une copie complète de p1 est affectée à p2 à l'aide de la méthode copy (). Cela implique que tout changement dans l'un d'eux ne doit avoir aucun effet sur l'autre.
  • Lignes 27 à 28: le nom de p2 et les valeurs de ville sont modifiés. Ceux-ci ne doivent pas refléter dans p1.
  • Ligne 30: le nom de p1 est comme prévu, mais sa ville? Il devrait être "Mumbai" ne devrait pas? Mais nous ne pouvons pas voir cela se produire. "Bangalore" était seulement pour p2 non? Ouais… exactement.

Copie profonde…! Ce n'était pas prévu de votre part. Vous avez dit que vous allez tout copier. Et maintenant tu te comportes comme ça. Pourquoi oh pourquoi..?! Qu'est-ce que je fais maintenant?

Ne paniquez pas. Voyons ce que les adresses mémoire doivent dire à ce sujet.

De l'illustration ci-dessus, nous pouvons voir que

  • p1 et p2 pointent vers différents emplacements de mémoire comme prévu.
  • Mais leurs variables d'adresse pointent toujours vers le même emplacement. Cela signifie que même après les avoir copiées en profondeur, seules les références sont copiées, c'est-à-dire une copie superficielle bien sûr.

Remarque: chaque fois que nous copions un type de référence, une copie superficielle est créée par défaut jusqu'à ce que nous spécifiions explicitement qu'elle doit être copiée en profondeur.

func copy (avec zone: NSZone? = nil) -> N'importe quel
{
    let person = Person (self.name, self.address)
    personne de retour
}

Dans la méthode ci-dessus que nous avons implémentée précédemment pour la classe Person, nous avons créé une nouvelle instance en copiant l'adresse avec self.address. Cela ne fera que copier la référence à l'objet adresse. C’est la raison pour laquelle les adresses p1 et p2 pointent au même endroit.

Ainsi, la copie de l’objet à l’aide de la méthode copy () ne créera pas une copie conforme de l’objet.

Pour dupliquer complètement un objet de référence: le type de référence ainsi que tous les types de référence imbriqués doivent être copiés avec la méthode copy ().

let person = Person (self.name, self.address.copy () as? Address)

Utiliser le code ci-dessus dans la copie de func (avec zone: NSZone? = Nil) -> Toute méthode fonctionnera correctement. Vous pouvez voir cela dans l'illustration ci-dessous.

True Deep Copy - Types de référence et de valeur

Nous avons déjà vu comment créer une copie complète des types de référence. Bien sûr, nous pouvons le faire avec tous les types de référence imbriqués.

Mais qu'en est-il du type de référence imbriqué dans un type de valeur, c'est-à-dire un tableau d'objets ou une variable de type de référence dans une structure ou peut-être un tuple? Pouvons-nous résoudre cela aussi avec copy ()? Non, nous ne pouvons pas, en fait. La méthode copy () nécessite la mise en œuvre du protocole NSCopying qui ne fonctionne que pour les sous-classes NSObject. Les types de valeur ne supportent pas l’héritage, nous ne pouvons donc pas utiliser copy () avec eux.

Dans la ligne 2, seule la structure d'arr1 est copiée en profondeur, mais les objets d'adresse qui s'y trouvent sont toujours copiés de manière superficielle. Vous pouvez le voir sur la carte mémoire ci-dessous.

Les éléments à la fois arr1 et arr2 pointent vers les mêmes emplacements de mémoire. Cela est dû à la même raison: les types de référence sont copiés peu profonds par défaut.

La sérialisation puis la désérialisation d'un objet crée toujours un nouvel objet. Il est valable pour les deux types de valeur ainsi que pour les types de référence.

Voici quelques API que nous pouvons utiliser pour sérialiser et désérialiser des données:

  1. NSCoding - Protocole permettant de coder et de décoder un objet en vue de son archivage et de sa distribution. Cela fonctionnera uniquement avec les objets de type classe car il nécessite l'héritage de NSObject.
  2. Codable - Rendez vos types de données codables et décodables pour assurer la compatibilité avec des représentations externes telles que JSON. Cela fonctionnera pour les deux types de valeur - struct, tableau, tuple, types de données de base et types de référence - classe.

Restaurez encore un peu la classe Address pour vous conformer au protocole Codable et supprimez tout le code NSCopying que nous avons ajouté précédemment dans l’extrait de code 3.

Dans le code ci-dessus, les lignes 11 à 13 créeront une véritable copie en profondeur d’arr1. Ci-dessous, l'illustration donne une image claire des emplacements de mémoire.

Copier en écriture

La copie à l'écriture est une technique d'optimisation qui permet d'améliorer les performances lors de la copie de types de valeur.

Disons que nous copions un seul String, Int ou peut-être tout autre type de valeur - nous ne rencontrerons aucun problème de performance crucial dans ce cas. Mais qu'en est-il lorsque nous copions un tableau de milliers d'éléments? Ne créera-t-il toujours pas de problèmes de performances? Que se passe-t-il si nous la copions simplement sans apporter de modifications à cette copie? La mémoire supplémentaire que nous avons utilisée n’est-elle pas un gaspillage dans ce cas?

Voici le concept de Copie en écriture - lors de la copie, chaque référence pointe vers la même adresse mémoire. Ce n’est que lorsque l’une des références modifie les données sous-jacentes que Swift copie réellement l’instance originale et effectue la modification.

C’est-à-dire qu’il s’agisse d’une copie profonde ou superficielle, une nouvelle copie ne sera pas créée tant que nous n’aurons pas modifié l’un des objets.

Dans le code ci-dessus,

  • Ligne 2: une copie complète d'arr1 est assignée à arr2
  • Lignes 4 et 5: arr1 et arr2 pointent toujours sur la même adresse mémoire
  • Ligne 7: modifications effectuées dans arr2
  • Lignes 9 et 10: arr1 et arr2 pointant maintenant vers différents emplacements de mémoire

Maintenant, vous en savez plus sur les copies profondes et superficielles et sur leur comportement dans différents scénarios avec différents types de données. Vous pouvez les essayer avec votre propre ensemble d'exemples et voir quels résultats vous obtenez.

Lectures complémentaires

N'oubliez pas de lire mes autres articles:

  1. Tout sur le Codable dans Swift 4
  2. Tout ce que vous avez toujours voulu savoir sur les notifications dans iOS
  3. Colorez-le avec les GRADIENTS - iOS
  4. Codage pour iOS 11: comment glisser-déposer dans les collections et les tables
  5. Tout ce que vous devez savoir sur les extensions Today (widget) dans iOS 10
  6. Sélection UICollectionViewCell rendue facile .. !!

N'hésitez pas à laisser des commentaires au cas où vous auriez des questions.