Bonjour chers écrivains non-littéraires ! Aujourd’hui on attaque notre dernier chapitre sur la POO et j’espère que vous êtes comme votre café fraichement moulu et filtré : chaud ! Nous allons nous intéresser aux énumérations et leur pouvoir organisationnel débordant pour avoir encore plus de structure et de précision dans notre code qui devient chaque jour un peu plus complexe. Comment ordonner ses données ? Pourquoi le faire ? L’ordre est-il dans les choses ou dans l’esprit ? C’est parti !
Pourquoi faire ?
Non en effet les questions de l’introduction ne sont pas dans l’ordre (quel comble !) et il y en a même une que l’on en traitera pas (demander un remboursement). Elucubrations mises à part, les énumérations sont un moyen de lister et d’organiser facilement différents cas ou états que peut avoir une variable. C’est un peu comme un booléen, mais avec plus de valeurs possibles que vrai et faux. Par exemple, si on pose la question “Etes vous musicien ?” on pourrait imagine stoker sa réponses sous forme d’un booléen : oui ou non. Mais si on pose une question avec un peu plus de réponses possibles, disons “Vivons-nous dans une simulation ?” (oui je suis d’humeur existentialiste) alors on devra prévoir un peu plus de cas de réponses possibles, disons :
- Oui
- Non
- Oui et non
- Ne se prononce pas
- Qu’est ce qu’une simulation ?
- Je klaxonne
Alors tout de suite un booléen semble un peu limité pour récupérer une catégorisation aussi diverse et complexe que ça. Bien sûr, on pourrait imaginer stocker cette réponse sous forme de texte ou de nombre (oui = 1, non = 2, etc…) mais cela va poser plusieurs problèmes fonctionnels dès lors que l’on va s’en servir et le passer de fonction en fonction car il sera possible d’attribuer une valeur de réponse qui n’existe pas. Par exemple on pourra imaginer une erreur dans le code ou une modifications ultérieures qui attribue -1 et ainsi crée des problèmes en cascade dans le programme. En plus de cela, il faudra se rappeler exactement à quoi correspond quel chiffre lorsqu’on s’en servira, et il faudra relire tout le code lorsqu’on changera le moindre de ses états pour s’assurer que les changements se répercutent bien de partout. Bref, que des problème ! C’est donc pour cela que les énumérations sont utiles, elles permettent de stocker une multitudes d’états et de les regrouper dans un type bien défini.
S’en servir
Pour déclarer une énumération, c’est comme quand on déclare n’importe quel autre objet en Swift, on commence par le mot-clé enum suivi de son nom (qui sera aussi son type) et puis on colle son contenu entre accolades. Le contenu sera majoritairement composé d’états ou bien cas qui seront déclarés avec le mot-clé case :
1 2 3 4 5 | enum Reponse { case oui case non case ouietnon } |
Plutôt simple pour l’instant ? Ici on a déclaré le type Reponse qui peut posséder 3 valeurs : oui, non, ouietnon. Pour nous en servir, on va y accéder comme à un sous-object quelconque (comme une variable de classe) directement dans le type Reponse. Ce qu’il faut bien comprendre c’est que l’on n’accède pas à un objet initialisé qui serai contenu dans une variable, on fait référence à la valeur d’un type global, de la même façon qu’une propriété statique dans une classe. Notons d’ailleurs qu’il est possible de créer des variables et fonctions statiques dans une énumération, comme dans une classe.
1 2 3 | let reponse = Reponse.oui // OU BIEN let reponse: Reponse = .oui |
Une fois la valeur récupérée et pourquoi pas stockée comme ici dans une constante, on peut s’en servir comme de n’importe quelle autre valeur typée et la stocker dans des tableaux, la passer en argument de fonction, la comparer, etc…
Bien souvent on va s’en servir lors de comparaison, et c’est là que l’on va pouvoir profiter des avantages d’un énumération, notamment dans un switch qui va pouvoir facilement regrouper toutes les valeurs d’un type et ainsi éviter d’oublier ou de mélanger lors modifications ultérieures :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | let reponse = Reponse.oui if reponse == .oui { // exemple de condition simple } switch reponse { case .oui: // code case .non: // code case .ouietnon: // code } |
En effet, un switch lorsqu’il est utilisé avec une énumération devra être exhaustif sinon il nous retournera une erreur. Si on oublie de lister un cas, Xcode va nous afficher une erreur nous rappelant que Switch must be exhaustive et nous proposera de remplir automatiquement les cas manquants, pratique en cas de modification des valeurs de l’énumération ! Notons tout de même qu’il n’est pas obligatoire de lister toutes les valeurs possibles si on fournit une exécution par défaut à notre switch :
1 2 3 4 5 6 | switch reponse { case .oui: // code default: // code } |
Les valeurs brutes
On peut faire hériter certains protocoles à une énumération pour lui permettre d’être manipulée de différentes manières. Par exemple, on peut lui attribuer une valeur brute qui permettra de récupérer une valeur associée à n’importe quel moment, ou bien d’initialiser une variable d’état à partir de sa valeur brute. En clair, si on prend par exemple les nombre, on peut associer un nombre à chaque état de l’énumération, et initialiser chacun des états depuis un nombre :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | enum Reponse: Int { case oui = 0 case non // automatiquement 1 case ouietnon // automatiquement 2 } let reponse1 = Reponse.oui print(reponse1) // oui print(reponse1.rawValue) // 0 let reponse2 = Reponse.init(rawValue: 1) print(reponse2) // Optionnal(non) print(reponse2?.rawValue) // Optionnal(1) |
On notera que l’initialisation depuis une valeur brute dans reponse2 produit une variable de type optionnel car il est possible qu’aucun cas n’y corresponde
On peut faire la même chose avec des String ou des caractères uniques :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | enum Reponse: String { case oui // automatiquement oui case non // automatiquement non case ouietnon = "peut-être" } let reponse1 = Reponse.oui print(reponse1) // oui print(reponse1.rawValue) // oui let reponse2 = Reponse.init(rawValue: "peut-être") print(reponse2) // Optionnal(ouietnon) print(reponse2?.rawValue) // Optionnal(peut-être) enum ASCIIControlCharacter: Character { case tab = "\t" case nouvelleLigne = "\n" } // etc... |
Mais en plus de valeurs brutes, il est possible d’hériter de types permettant certaines opérations, nous allons en voir deux particulièrement utiles :
Comparable
Sans grand mystère, cela permet la comparaison entre deux états de l’énumération. Par défaut, Swift prendra en compte l’ordre dans lequel les états sont déclarés, donc dans notre exemple l’ordre de déclaration est croissant :
1 2 3 4 5 6 7 | enum Reponse: Comparable { case oui case non case ouietnon } print(Reponse.oui < Reponse.non) // true |
Il est possible d’ajouter manuellement ce comportement sans ce protocole ajoutant ces fonctions (et leur code personnalisé) dans la déclaration d’énumération elle-même
1
2
3
4 static func < (lhs: Self, rhs: Self) -> Bool
static func <= (lhs: Self, rhs: Self) -> Bool
static func >= (lhs: Self, rhs: Self) -> Bool
static func > (lhs: Self, rhs: Self) -> Bool
CaseIterable
En ajoutant ce simple protocole, il est possible de lister tous les états de l’énumération dans un tableau en appelant la propriété allValue, pratique !
1 2 3 4 5 6 7 | enum Reponse: CaseIterable { case oui case non case ouietnon } print(Reponse.allCases) // [.oui, .non, .ouietnon] |
Et voilà ! Nous avons terminé notre chapitre sur les énumérations et même notre cours sur la programmation orientée objet ! Hourra ! Il reste encore des choses à découvrir mais vous possédez désormais tout ce qui est nécessaire pour manipuler des objets de manière efficace en Swift, ce qui est un grand pas en avant pour comprendre la création d’apps complexes. Bravo à vous et on se retrouvera bientôt pour un exercice synthétique de tout ça 🙂
Excellent Article, Excellent Blog , Excellent Site ✅✅✅