Fiche 00.01 — Struct vs class : value types et reference types
Objectif
Maîtriser la distinction value type (struct/enum) vs reference type (class) : copie vs partage, identité vs égalité, copy-on-write. C'est le socle mental de SwiftUI (pourquoi une View est une struct) et LA question d'entretien iOS n°1.
1. Le résumé en une phrase
- Value type (
struct,enum, et les tuples) : copié à chaque affectation ou passage en paramètre. Chaque variable possède sa propre instance. - Reference type (
class) : partagé. Une variable contient une référence (un pointeur) vers une instance unique sur le heap. Deux variables peuvent pointer vers le même objet.
Pont UIKit : en UIKit, tout ce que tu manipules est une class (UIView, UIViewController, UILabel, NSObject…). Tu raisonnais donc en permanence en "référence partagée". En Swift moderne, le défaut s'inverse : tu modélises avec des struct, et la class devient le cas particulier.
2. Copie (struct) vs partage (class) — l'exemple chiffré
Le réflexe à graver : avec une struct, b = a clone. Avec une class, d = c partage. Modifier d, c'est modifier c.
Note : c et d sont déclarés en let mais on peut quand même muter d.x. Avec une class, let verrouille la référence (tu ne peux pas réassigner c = autreInstance), pas le contenu de l'objet pointé.
3. Passage en paramètre : même règle
Les paramètres struct arrivent en let (immuables) par défaut ; tu en fais une copie locale var p = p si tu veux les muter. C'est l'opposé du inout (voir plus bas).
4. Identité (===) vs égalité (==)
Deux notions différentes, souvent confondues en entretien.
==: égalité de valeur. Tu l'obtiens en conformant ton type àEquatable(souvent synthétisé automatiquement pour les struct).===: identité de référence = "est-ce le même objet en mémoire ?". Réservé auxclass. N'existe pas pour les struct (ça n'aurait aucun sens : une struct n'a pas d'identité).
Pont UIKit : === est ton ancien if view == someView au sens pointeur (Objective-C comparait les pointeurs). En Swift, ce pointeur-check s'écrit explicitement ===.
5. Copy-on-write (CoW) : le compromis perf
Si chaque copie de struct dupliquait physiquement la mémoire, un Array de 100 000 éléments serait catastrophique à passer en paramètre. Swift triche avec le copy-on-write sur Array, Dictionary, Set, String (et tes propres types qui s'appuient dessus).
Principe : la copie est logique mais le stockage est partagé physiquement tant que personne ne mute. La duplication réelle ne se déclenche qu'à la première mutation (et seulement si le buffer est partagé).
Implication perf concrète : copier un gros tableau pour le lire est gratuit. Le coût n'apparaît qu'au moment où tu écris. Tu peux observer le déclenchement avec isKnownUniquelyReferenced(&ref) quand tu implémentes ton propre type CoW.
Piège fréquent : muter en boucle un élément d'un tableau passé/copié peut déclencher des copies inutiles si le buffer est encore partagé. En pratique, garde la mutation localisée à un seul propriétaire.
6. mutating func, let vs var, immutabilité
Une méthode de struct qui modifie ses propres propriétés doit être marquée mutating (elle remplace self par une nouvelle valeur, conceptuellement).
let sur une struct = immutabilité totale : aucune mutating func, aucune écriture de propriété. C'est garanti par le compilateur, gratuitement. Sur une class, let ne fige que la référence (rappel §2).
Quand tu veux qu'une fonction modifie la struct de l'appelant (au lieu de travailler sur une copie), utilise inout :
7. Pourquoi une SwiftUI View est une struct (le pont clé)
En UIKit, une UIView est une class : lourde, mutable, avec un cycle de vie long, manipulée par référence (addSubview, removeFromSuperview…). Tu gardais des références et tu mutais l'arbre de vues.
En SwiftUI, une View est une struct jetable :
SwiftUI recrée cette struct très souvent (à chaque invalidation). C'est voulu et bon marché précisément parce que c'est une struct légère, sans identité, sans héritage. Le framework compare l'ancienne et la nouvelle valeur (value type → comparaison de valeur facile) pour décider quoi redessiner. L'état persistant, lui, ne vit pas dans la struct : il est externalisé dans @State, @Observable, etc.
Réponse type en entretien : "Une View est une struct car elle décrit l'UI comme une valeur recalculée à la demande, pas un objet mutable à long terme. Ça rend le diffing déclaratif efficace et évite les bugs d'état partagé d'UIKit."
8. Quand choisir struct vs class
Struct par défaut. C'est la recommandation Apple. Modèles de données, DTO, valeurs, et toutes tes Views.
Choisis une class seulement si tu as besoin d'un de ces points :
- Identité : deux objets de même valeur doivent rester distincts (un compte bancaire, une session).
- État partagé / mutable depuis plusieurs endroits : un cache, un coordinateur, un service.
- Cycle de vie partagé : plusieurs parties de l'app pointent vers le même objet et observent ses changements.
- Héritage : tu dois étendre une classe (souvent imposé par UIKit/Foundation, ex.
UIViewController). - Interop Objective-C /
@objc.
Cas SwiftUI moderne : un ViewModel est une class, en @Observable (macro Observation, iOS 17+) :
Pourquoi class ici ? Parce qu'on veut une identité stable et un état partagé observable qui survit aux recréations de la View. La struct (la View) est jetable ; la class (le ViewModel) persiste et notifie. C'est la division du travail centrale en SwiftUI.
(Legacy à connaître : avant iOS 17 on utilisait ObservableObject + @Published. @Observable le remplace : moins de boilerplate, et le redessin se déclenche seulement sur les propriétés réellement lues par la View.)
Points à connaître
letsur une class ne fige que la référence, pas le contenu — piège n°1 des candidats. Sur une struct,letfige tout.===n'existe que pour les class. Le proposer sur une struct est une erreur conceptuelle (une struct n'a pas d'identité).- Le copy-on-write rend les copies de gros
Array/Stringgratuites en lecture ; le coût n'arrive qu'à la première mutation d'un buffer partagé. - Mettre tout l'état mutable dans une struct partagée "comme en UIKit" → bugs : tu modifies une copie en croyant modifier l'original. Si tu veux du partage, c'est une class (ou un binding).
Exercice
Prédis sans compiler la sortie de ce snippet, puis vérifie dans un Playground :
Justifie chaque ligne en une phrase (copie vs partage, identité).
Question d'entretien
Q : "Struct ou class, comment tu choisis ?"
R : "Struct par défaut, conformément aux guidelines Apple : value semantics, pas d'état partagé surprise, copie sûre, et c'est ce qu'attend SwiftUI pour ses Views. Je passe à une class uniquement si j'ai besoin d'identité, d'héritage, d'interop Objective-C, ou d'un état mutable partagé observable — typiquement un ViewModel @Observable ou un service singleton."
Q : "Quelle est la différence entre == et === ?"
R : "== compare la valeur (via Equatable), === compare l'identité, c'est-à-dire si deux variables pointent vers le même objet en mémoire. === n'a de sens que pour les class."
Q : "Pourquoi une SwiftUI View est-elle une struct alors qu'une UIView est une class ?"
R : "Parce qu'une View décrit l'UI comme une valeur recalculée fréquemment et jetée. Une struct légère sans identité rend ces recréations bon marché et le diffing déclaratif simple. L'état persistant est externalisé dans @State ou un ViewModel @Observable, pas stocké dans la struct."
Résumé
- Value type (struct/enum) = copié à l'affectation et au passage ; chaque variable a sa propre instance.
- Reference type (class) = partagé ; plusieurs variables peuvent pointer vers la même instance.
=== égalité de valeur (Equatable) ;==== identité, réservé aux class.- Copy-on-write : copie logique, partage physique du buffer jusqu'à la première mutation (
Array,String,Dictionary,Set). letfige tout sur une struct, seulement la référence sur une class ;mutating funcpour modifier une struct.- Struct par défaut ; class pour identité, héritage, interop ObjC, ou état partagé observable (ViewModel
@Observable). - En SwiftUI, la View est une struct jetable, le ViewModel est une class persistante — d'où l'inversion du réflexe UIKit (où tout était class).