Découverte de Swift (4)
Je continue ma découverte de Swift à travers la lecture de “The Swift Programming Language (Swift 5.3)”. Au programme aujourd'hui : les énumérations.
Enumération simple
Une énumération, pour faire simple, c'est une ... énumération de valeurs possibles. Par exemple, je pourrais définir une énumération listant les planètes du système solaire (attention, il y a un piège) :
enum Planet {
case mercury
case venus
case earth
case mars
case jupiter
case saturn
case uranus
case neptune
case pluto
}
Une fois cette énumération définie, je peux l'utiliser pour tester ses valeurs. Par exemple dans un switch pour afficher un message personnalisé pour certaines planètes ou un message défaut pour les autres :
func printPlanet(planet: Planet) {
switch planet {
case .earth:
print("Home sweet home")
case .mars:
print("The red planet ! I'd love to go there one day")
case .pluto:
print("Is it really a planet ?")
default:
print("Not a safe place for humans")
}
}
var myPlanet = Planet.earth
printPlanet(planet: myPlanet) // Prints "Home sweet home"
myPlanet = Planet.jupiter
printPlanet(planet: myPlanet) // Prints "Not a safe place for humans"
Enumération avec valeurs associées
Pour un cas d'usage un peu moins simple, je peux définir une énumération pour gérer des formats de code-barres différents pour les articles d'un magasin :
enum BarCode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
Avec cette déclaration, j'indique qu'un BarCode
a deux formats possibles :
– un premier au format UPC, encodé sous la forme de 4 nombres entiers
– un second qui est un QR code, encodé sous la forme d'une chaîne de caractères
Je peux ensuite créer une fonction qui va afficher le contenu d'un code-barres, en utilisant un switch
où chaque case
correspond à l'un des formats de mon énumération BarCode
:
func printBarCode(barCode: BarCode) {
switch barCode {
case let .upc(numberSystem, manufacturer, product, check):
print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
print("QR code: \(productCode).")
}
}
A partir de là, il devient aisé de gérer des articles ayant des formats de code-barres différents :
var myBarCode = BarCode.upc(8, 85909, 51226, 3)
printBarCode(barCode: myBarCode)
// Prints "UPC: 8, 85909, 51226, 3."
myBarCode = .qrCode("ABCDEFGHIK")
// Inutile de repréciser le type BarCode car le type a été défini lors de la première affectation
printBarCode(barCode: myBarCode)
// Prints "QR code: ABCDEFGHIK."
Dans le cas d'une énumération avec valeurs associées, chaque case
définit un format de données, dont les valeurs peuvent changer pour chaque constante ou variable ayant pour type l'énumération en question.
Enumération avec valeurs brutes
Il est possible de définir une valeur brute pour chaque case
d'une énumération, comme dans cet exemple simple où nous listons quelques caractères ASCII spéciaux :
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
Enumération avec valeurs brutes implicites
Toujours dans le même ordre d'idée, Swift peut déterminer de façon implicite la valeur brute à partir de type d'élément déclaré.
Si on reprend notre système solaire du début, on peut réécrire la définition en précisant que l'énumération gère des valeurs brutes de type Int
:
enum NumberedPlanet: Int {
case mercury
case venus
case earth
case mars
case jupiter
case saturn
case uranus
case neptune
case pluto
}
Dans ce cas, Mercure se voit implicitement affecter la valeur 0
, Venus la valeur 1
, etc. Si je veux commencer l'énumération à partir de la valeur 1
, il suffit de définir la valeur brute du premier élément, les suivants sont implicitement définis de façon automatique :
enum NumberedPlanet: Int {
case mercury = 1
case venus
case earth
case mars
case jupiter
case saturn
case uranus
case neptune
case pluto
}
De cette façon, la Terre reste bien la 3ème planète du système solaire :
print(NumberedPlanet.earth.rawValue) // Prints 3
Initialisation avec la valeur brute
Une fois définies les valeurs brutes d'une énumération, il est possible de créer une instance de cette énumération à partir d'une valeur brute.
Dans cet exemple, je crée une fonction qui reçoit en paramètre la position dans le système solaire de la planète désirée et qui affiche un message personnalisé pour cette planète :
func printNumberedPlanet(_ position: Int) {
if let planet = NumberedPlanet(rawValue: position) {
switch planet {
case .earth:
print("Home sweet home")
case .mars:
print("The red planet ! I'd love to go there one day")
case .pluto:
print("Is it really a planet ?")
default:
print("Not a safe place for humans")
}
}
else {
print("There isn't a planet at position \(position)")
}
}
La fonction reçoit en paramètre un entier position
qui contient la position de la planète dans le système solaire.
La première étape est d'essayer de créer une instance de l'énumération NumberedPlanet
à partir de la position reçue. Nous utilisons pour cela la fonctionnalitré d'optional binding que nous avons vue dans un billet précédent. Cela se passe ici :
if let planet = NumberedPlanet(rawValue: position) {
...
}
else {
...
}
}
L'initialisateur de NumberedPlanet
à partir d'un entier renvoie un Int?
, c'est-à-dire un optional Int
. Nous utilisons donc la syntaxe let ... = ...
pour vérifier si l'optional contient une valeur ou non.
Si la position reçue en paramètre correspond à une planète solaire, nous pouvons utilisons la constante planet
que nous venons de créer, sinon nous sommes sur un cas d'erreur que nous pouvons facilemement gérer avec un message particulier :
printNumberedPlanet(4)
// Prints "The red planet ! I'd love to go there one day"
printNumberedPlanet(11)
// Prints "There isn't a planet at position 11"
Enumération récursive
Une énumération récursive est une énumération qui contient une autre instance d'elle-même comme une valeur associée.
Prenons l'exemple d'un expression arithmétique simple :
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}
Elle peut se composer de 3 façons :
– un simple nombre entier Int
– une addition qui combine elle-même 2 expressions arithmétiques
– une multiplication qui combine elle-même 2 expressions arithmétiques
Si je veux modéliser l'opération (5 + 4) * 2
sous ce format, je vais procéder ainsi :
// ( 5 + 4 ) * 2
let five = ArithmeticExpression.number(5) // 5
let four = ArithmeticExpression.number(4) // 4
let sum = ArithmeticExpression.addition(five, four) // 5 + 4
let two = ArithmeticExpression.number(2) // 2
let product = ArithmeticExpression.multiplication(sum, two) // ( 5 + 4) * 2
A la fin, j'ai bien une ArithmeticExpression
nommée product
qui contient l'opération (5 + 4) * 2
et qui se décompose de la façon suivante :
– product
est une multiplication
de sum
et de two
-
twoest le nombre entier 2
-
sumest une
additionde
fiveet de
four
-
fiveest le nombre entier 5
-
four` est le nombre entier 4
Sur cette base, je peux ensuite définir une fonction qui va réaliser les opérations modélisées par les instances de l'énumération ArithmeticExpression
:
func evaluate(_ expression: ArithmeticExpression) -> Int {
switch expression {
case let .number(value):
return value
case let .addition(left, right):
return evaluate(left) + evaluate(right)
case let .multiplication(left, right):
return evaluate(left) * evaluate(right)
}
}
print(evaluate(product))
// Prints "18"
La fonction evaluate
est elle-même récursive : elle s'appelle elle-même pour les additions et les multiplications, jusqu'à ce que toutes les ArithmeticExpression
soient de simples nombres entiers.
Et la suite ?
Au programme du prochain chapitre du livre, et sans doute du prochain billet : les structures et les classes. J'ai hâte de lire ça !
Zéro Tech, le blog Tech de Zéro Janvier – @zerojanvier@mamot.fr