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 uneadditiondefiveet defour -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 !

#swift

Zéro Tech, le blog Tech de Zéro Janvier@zerojanvier@diaspodon.fr