2

POO : Les structures

Cet article n'a pas été mis à jour depuis plus d'un an, il est possible que certaines informations ne soient plus à jour. Si vous rencontrez des erreurs ou des différences en le suivant, n'hésitez pas à commenter pour me le signaler.

Bonjour / bonsoir les affamés de programmation dans le meilleur langage qui permet de le faire (totalement objectif). Nous voici de retour enfin pour de nouvelles aventures, nous allons pouvoir nous pencher après tout ce temps (désolé pour ceux qui ne suivent pas en temps réel) pour en découvrir toujours plus sur notre dada commun ! Au menu aujourd’hui : un nouvel objet très similaire aux classes mais suffisamment différent pour y consacrer du temps. Voici donc une plongée dans les structures en Swift !

Prenons comme base une classe

Pour comprendre ce qu’est une structure, nous allons partir d’une classe. Les deux objets ont en effet de nombreuses similitudes :

  • Contiennent des propriétés (variables)
  • Contiennent des méthodes (fonction)
  • Supportent les propriétés statiques
  • Ont des méthodes d’initialisation personnalisée (init())
  • Se conforment à des protocoles
  • Supportent les sous-scripts et les extensions (pas encore abordés)

Vu d’ici, il n’y a pas tant de différences que ça, alors essayons. Créons une classe qui représente des produits en stock dans un magasin :

1
2
3
4
5
6
7
8
9
10
class Rayon {
    var allee: Int
    var etage: Int
}

class Produit {
    var nom : String
    var prix: Int
    var rayon: Rayon
}

Vu que vous êtes des bons élèves, vous vous êtes rendu compte que ce code no fonctionne pas du tout. En effet il manque un élément important : les initialiseurs. Sans cela, Xcode nous rappelle gentiment que Class 'Produit' has no initializers (et Rayon aussi). Alors créons-en un pour essayer :

1
2
3
4
5
init(nom: String, prix: Int, rayon: Rayon) {
    self.nom = nom
    self.prix = prix
    self.rayon = rayon
}

Tout de suite on s’aperçoit de quelque chose, cet initialiseur semble un peu… inutile pour le moins ! On a trois propriétés dans notre classe, on passe trois arguments et on les assignent machinalement. Le meilleur moyen de résoudre ce problème est (vous l’avez deviné) : une structure. En effet si l’on transforme notre classe en structure, le compilateur Swift nous intègrera automatiquement un initialiseur comprenant toutes nos propriétés. Faisons cela pour voir un peu :

1
2
3
4
5
6
7
8
9
10
11
12
struct Rayon {
    var allee: Int
    var etage: Int
}

struct Produit {
    var nom: String
    var prix: Int
    var rayon: Rayon
}

let produit = Produit(nom: "Café", prix: 1, rayon: Rayon(allee: 12, etage: 3))

Aucune erreur ! On peut directement initialiser sans avoir à écrire du code redondant, et cela même si on assigne des valeurs par défaut à nos variables. Dans ce cas, le compilateur nous créera tous les initialiseurs nécessaires dont nous pourrions avoir besoin pour créer une nouvelle instance de notre structure le plus simplement possible :

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Rayon {
    var allee: Int
    var etage: Int = 1
}

struct Produit {
    var nom: String
    var prix: Int = 0
    var rayon: Rayon
}

let cafe = Produit(nom: "Café", prix: 1, rayon: Rayon(allee: 12, etage: 3))
let pates = Produit(nom: "Pâtes", rayon: Rayon(allee: 6))

Pratique non ? Tout cela en conservant la possibilité d’écrire un init() rien qu’à nous qui initialiserai les propriétés comme on le ferait dans une classe standard. On peut même y ajouter des méthodes et y accéder de la même façon que dans une classe :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Produit {
    var nom: String
    var prix: Int = 0
    var rayon: Rayon
   
    func nestPasTropHaut() -> Bool {
        if self.rayon.etage < 2 {
            return true
        }
        return false
    }
}

let cafe = Produit(nom: "Café", prix: 1, rayon: Rayon(allee: 12, etage: 3))
let pates = Produit(nom: "Pâtes", rayon: Rayon(allee: 6))

print(cafe.nestPasTropHaut())   // false
print(pates.nestPasTropHaut())  // true

Et on pourrait ainsi réécrire tout ce que l’on a déjà dit sur les classes, les deux objets sont absolument semblables, malgré quelques différences. Eh oui, vous vous doutez bien que si il ya deux objets, c’est qu’ils ne servent pas à la même chose. Voyons un peu en quoi les structures sont différentes de classes.

Les structures sont immuables

On vient de voir que déjà les structures nous génèrent des initialiseurs pré-cuits pour nous régaler de nos variables (vous me dites si je vais trop loin) et ainsi gagner en simplicité et donc en lisibilité. Une autre différence majeure est sur l’héritage que les structures peuvent avoir. En effet, Nous avons vu dans le cours sur l’héritage de classe que ces dernières pouvaient hériter de tout un tas de types, eux mêmes déclarés par d’autre classes ou protocoles. Cependant une structure ne pourra pas hériter d’une classe ou d’une autre structure, son héritage se limitera donc aux simples protocoles. En revanche, il es possible de cumuler plusieurs protocoles sur une seule et même structure.

Néanmoins la différence fondamentale est sur la mutabilité des structures. Ces dernières sont par défaut immuables et sont donc considérés comme des constantes et ce avant même leur utilisation. Cela veut notamment dire que par défaut, les valeur de leurs propriétés ne sont pas modifiables depuis les méthodes internes. Prenons un exemple concret, créons une méthode qui augmente de 1 la valeur d’une des propriétés de notre structure Rayon :

1
2
3
4
5
6
7
8
struct Rayon {
    var allee: Int
    var etage: Int = 1
   
    func bougerEnHaut() {
        self.etage += 1 // ERROR : Left side of mutating operator isn't mutable: 'self' is immutable
    }
}

Si l’on copie-colle ce code dans Xcode nous allons avoir une erreur qui nous indique que l’on ne peut pas modifier la valeur d’une propriété. Par défaut les propriété des structures sont considérés comme des constantes une fois initialisées dans le but d’optimiser l’utilisation de la RAM. Pour modifier ce comportement, il nous faut déclarer que notre méthode est capable d’utiliser la structure et ses propriétés comme des variables grâce au mot-clé : mutating.

1
2
3
4
5
6
7
8
struct Rayon {
    var allee: Int
    var etage: Int = 1
   
    mutating func bougerEnHaut() {
        self.etage += 1
    }
}

De cette façon on autorise la méthode à modifier des valeurs, mais ce au prix d’un coût de calcul (légèrement) plus important. En effet lorsque cela se produit, Swift va en arrière plan supprimer notre instance de structure et en créer une nouvelle avec les propriétés modifiées. Pour bien comprendre ce comportement qui semble si spécial, voyons une autre application de cet exemple. Si l’on crée une nouvelle instance de notre structure dans une constante, il ne nous sera pas possible de modifier les valeurs de ses propriétés une fois que la structure sera instanciée :

1
2
3
4
5
6
7
8
9
10
struct Rayon {
    var allee: Int
    var etage: Int = 1
}

let emplacement = Rayon(allee: 1)
emplacement.allee = 2 // ERROR : Cannot assign to property: 'emplacement' is a 'let' constant

var emplacement2 = Rayon(allee: 2)
emplacement2.allee = 3

Ici on voit que les propriétés de l’instance de la structure dans la constante emplacement sont elles-mêmes considérées comme des constantes, alors qu’elles sont bien déclarées comme variable. En revanche, si l’on instancie notre structure dans une variable comme emplacement2, alors il nous sera possible de modifier ses valeurs comme dans une classe. Notons que dans ce dernier cas, l’impact sur le coût de calcul sera le même qu’avec l’utilisation d’une méthode mutable.

1
2
3
4
5
6
7
class Rayon {
    var allee: Int = 0
    var etage: Int = 1
}

let emplacement = Rayon()
emplacement.allee = 2

Aucune erreur si l’on tente la même opération avec une classe !

Pas facile de bien se représenter ce comportement, l’idée c’est que les structures sont faites pour être utilisées comme des conteneurs de données organisés et immuables, par conséquent il n’est pas attendu que leurs valeurs soient modifiées une fois déclarées.

Tout ceci nous emmène à la dernière différence fondamentale qui découle de ce que nous venons d’observer (et franchement si vous l’avez deviné vous pouvez être fiers de vous !). Vu que les structure sont faites pour être des objets plus simples et plus légers à stocker dans la RAM que les classes, leur accès en est légèrement modifié notamment lorsque l’on effectue des copies de variables. Si par exemple j’instance une classe dans une variable, puis associe cette variable à une autre, elle pointera (oui les pointeurs existent en Swift et on parlera bientôt !) toujours sur la première instance que j’ai déclarée. Voyons ce que ça donne en code parce que je sens que vos yeux commencent à loucher :

1
2
3
4
5
6
7
8
9
10
class Rayon {
    var allee: Int = 0
    var etage: Int = 1
}

var emplacement = Rayon()
var copieEmplacement = emplacement
emplacement.allee = 2

print(copieEmplacement.allee) // affiche 2

En accédant à la valeur de la classe dans copieEmplacement, je ne fait que relayer la valeur de la variable emplacement. En revanche, puisque les structures sont non mutables ce comportement n’aura pas lieu et en associant une instance de structure à une autre variable, on ne fera que la copier :

1
2
3
4
5
6
7
8
9
10
struct Rayon {
    var allee: Int = 0
    var etage: Int = 1
}

var emplacement = Rayon()
var copieEmplacement = emplacement
emplacement.allee = 2

print(copieEmplacement.allee) // affiche 0

Tout cela parce que les structures sont considérées comme des constantes, elles seront donc copiées au lieu d’être relayées.

Quand se servir de quoi ?

Tout ceci est bien beau (et bien technique, ouais !) mais ça ne nous dit pas vraiment à quelle occasion on utilisera un objet plutôt que l’autre. Bien qu’il n’existe pas de règle absolue, il est conseillé de se servir de structures chaque fois qu’une classe n’est pas strictement nécessaire. La raison est qu’une structure consommera moins de place dans la RAM et de par sa nature immuable est moins susceptible de créer des erreurs. Les classes en revanche sont plus gourmande en mémoire vive mais sont de réels objets dynamiques faits pour être modifiés et altérés tout au long de leur cycle de vie pour un coût de calcul moindre. En pratique, les structures sont plus souvent utilisées comme un moyen de structurer des données complexes et d’y accéder simplement et sûrement grâce à leur type. Les classes vont elles concentrer les opérations complexes de manipulation de données, de calcul et d’interface.

 

Voilà les loulous j’espère que ça vous a plu et que c’était clair, n’hésitez pas à me le dire si il y a des points à éclaircir, croyez moi j’en ai autant bavé à écrire celui-là que vous à le lire ! On a presque terminé notre initiation à la POO ce qui veut dire que vous aurez bientôt toutes les bases nécessaires à la création d’une vraie app. Je sais que pour l’instant tout ce que l’on a vu est assez abstrait mais ce sont des bases nécessaires pour bien comprendre ensuite comment fonctionnent les interfaces, le réseau et tout ce qu’il nous sera possible de faire ! Je vous retrouve très bientôt pour de nouvelles aventures, en attendant je vais me prendre un chocolat chaud bien mérité !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *