En Swift, une énumération (ou "enum") est un type de données qui permet de définir une liste de valeurs constantes. Chacune de ces valeurs peut être utilisée pour représenter une catégorie ou une option particulière. Les énumérations peuvent également avoir des valeurs associées, ce qui permet de stocker des informations supplémentaires pour chaque option. Les énumérations sont utiles pour fournir une syntaxe claire et concise pour les opérations conditionnelles et les algorithmes en général. Découvrons cela en détail.

Les cas d'énumérations peuvent également spécifier des valeurs associées de n'importe quel type à stocker avec chaque valeur de cas différente.

Les énumérations adoptent de nombreuses fonctionnalités traditionnellement prises en charge uniquement par les classes, telles que les propriétés calculées pour fournir des informations supplémentaires sur la valeur actuelle de l'énumération et les méthodes d'instance pour fournir des fonctionnalités liées aux valeurs représentées par l'énumération.

Les énumérations peuvent également définir des initialiseurs pour fournir une valeur de cas initiale. Elles peuvent être étendues pour étendre leurs fonctionnalités au-delà de leur implémentation d'origine et peuvent se conformer aux protocoles pour fournir des fonctionnalités standards.

Syntaxe d'énumération

Vous introduisez des énumérations avec le mot-clé enum et placez leur définition entière dans une paire d'accolades :


enum SomeEnumeration {
    // Corps de l'énumération
}

Voici un exemple pour les quatre points principaux d'une boussole :


enum CompassPoint {
    case north
    case south
    case east
    case west
}

Les valeurs définies dans l'énumération (tels que north, south, east et west) sont ses cas d'énumération. Vous utilisez le mot-clé case pour introduire de nouveaux cas d'énumérations.

Plusieurs cas peuvent apparaître sur une seule ligne, séparés par des virgules :


enum Planet {
    case mercury, venus, earth, mars, jupiter, saturn, uranus
}

Chaque définition d'énumération définit un nouveau type. Comme les autres types de Swift, leurs noms (tels que CompassPoint et Planet) commencent par une majuscule.


var directionToHead = CompassPoint.west

Le type de directionToHead est déduit lorsqu'il est initialisé avec l'une des valeurs possibles de CompassPoint. Une fois directionToHead déclaré comme un CompassPoint, vous pouvez le définir sur une valeur CompassPoint différente en utilisant une syntaxe de point plus courte :


directionToHead = .east

Correspondance avec Switch

Vous pouvez faire correspondre des valeurs d'énumération individuelles avec une instruction switch :


directionToHead = .south

switch directionToHead {
    case .north:
        print("Beaucoup de planètes sont au nord")
    case .south:
        print("Attention aux pingouins")
    case .east:
        print("Où le soleil se lève")
    case .west:
        print("Où le ciel est bleu")
}
// "Attention aux pingouins"

Une instruction switch doit être exhaustive lors de l'examen des cas d'une énumération. Si case pour .west est omis, ce code ne se compile pas, car il ne prend pas en compte la liste complète des cas de CompassPoint.

Lorsqu'il n'est pas approprié de fournir un cas d'énumération pour chaque cas, vous pouvez fournir un cas default pour couvrir tous les cas qui ne sont pas traités explicitement :


let somePlanet = Planet.earth

switch somePlanet {
    case .earth:
        print("Parfait pour y vivre")
    default:
        print("Pas sûr pour les humains")
}
// "Parfait pour y vivre"

Itération sur des cas d'énumérations

Pour certaines énumérations, il est utile d'avoir une collection de tous les cas de cette énumération. Vous activez cela en écrivant CaseIterable après le nom de l'énumération. Swift expose une collection de tous les cas allCases en tant que propriété du type énumération :


enum Beverage: CaseIterable {
    case coffee, tea, juice
}
let numberOfChoices = Beverage.allCases.count
print("\(numberOfChoices) boissons disponible")
// "3 boissons disponible"

Dans l'exemple ci-dessus, vous écrivez Beverage.allCases pour accéder à une collection qui contient tous les cas de l'énumération Beverage. Vous pouvez utiliser allCases comme n'importe quelle autre collection - les éléments de la collection sont des instances du type énumération, donc dans ce cas ce sont des valeurs Beverage.


for beverage in Beverage.allCases {
    print(beverage)
}
// coffee
// tea
// juice

Valeurs associées

Les exemples précédents montrent comment les cas d'une énumération sont une valeur définie (et typée) à part entière. Vous pouvez définir une constante ou une variable sur Planet.earth et vérifier cette valeur ultérieurement. Cependant, il est parfois utile de pouvoir stocker des valeurs d'autres types à côté de ces valeurs de cas. Ces informations supplémentaires sont appelées une valeur associée, et elles varient chaque fois que vous utilisez ce cas comme valeur dans votre code.

Vous pouvez définir des énumérations Swift pour stocker les valeurs associées de n'importe quel type donné, et les types de valeurs peuvent être différents pour chaque cas de l'énumération si nécessaire.

Prenons l'exemple d'un système de suivi des stocks qui doit suivre les produits à l'aide de deux types de codes-barres différents. Certains produits sont étiquetés avec des codes-barres 1D au format UPC, qui utilisent les numéros 0 à 9. Chaque code-barres comporte un chiffre du système numérique, suivi de cinq chiffres du code du fabricant et de cinq chiffres du code du produit. Ceux-ci sont suivis d'un chiffre de contrôle pour vérifier que le code a été scanné correctement.

D'autres produits sont étiquetés avec des codes-barres 2D au format de code QR, qui peuvent utiliser n'importe quel caractère ISO 8859-1 et peuvent coder une chaîne jusqu'à 2 953 caractères.

Dans Swift, une énumération pour définir les codes-barres de produits de l'un ou l'autre type peut ressembler à ceci :


enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
}

Cela se traduit par : "Définir un type d'énumération appelé Barcode, qui peut prendre soit une valeur upc avec une valeur associée de type ( Int, Int, Int, Int), ou une valeur qrCode avec une valeur associée de type String."

Cette définition ne fournit aucun réel Int ou valeur String, elle définit simplement le type de valeurs associées que les constantes ou les variables Barcode peuvent stocker lorsqu'elles sont égales à Barcode.upc ou Barcode.qrCode.

Vous pouvez ensuite créer un nouveau code-barres en utilisant l'un ou l'autre type :


var productBarcode = Barcode.upc(8, 85909, 51226, 3)

Cela crée une nouvelle variable appelée productBarcode et lui attribue une valeur de Barcode.upc avec une valeur de tuple associée de (8, 85909, 51226, 3)

Vous pouvez attribuer au même produit un type de code-barres différent :


productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

Maintenant, l'original Barcode.upc et ses valeurs Int sont remplacés par le nouveau Barcode.qrCode et sa valeur de String.

On peut vérifier les différents types de codes-barres à l'aide d'une instruction switch :


switch productBarcode {
    case .upc(let numberSystem, let manufacturer, let product, let check):
        print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
    case .qrCode(let productCode):
        print("QR code: \(productCode).")
}
// "QR code: ABCDEFGHIJKLMNOP."

Si toutes les valeurs associées pour un cas d'énumération sont extraites en tant que constantes, ou si toutes sont extraites en tant que variables, vous pouvez placer une seule annotation var ou let avant le nom du cas, par souci de concision :


switch productBarcode {
    case let .upc(numberSystem, manufacturer, product, check):
        print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")
    case let .qrCode(productCode):
        print("QR code: \(productCode).")
}
// "QR code: ABCDEFGHIJKLMNOP."

Valeurs brutes

L'exemple des codes-barres montre comment les cas d'une énumération peuvent déclarer qu'ils stockent des valeurs associées de types différents.

A la place des valeurs associées, les cas d'énumérations peuvent être pré-remplis avec des valeurs par défaut (appelées valeurs brutes), qui sont toutes du même type :


enum ASCIIControlCharacter: Character {
    case tab = "\t"
    case lineFeed = "\n"
    case carriageReturn = "\r"
}

Ici, les valeurs brutes d'une énumération appelée ASCIIControlCharacter sont définies comme étant de type Character et sont définies sur certains des caractères de contrôle ASCII les plus courants.

Les valeurs brutes peuvent être des String, des Character ou tout type de nombre Int ou Float. Chaque valeur brute doit être unique dans sa déclaration d'énumération.

Les valeurs brutes ne sont pas les mêmes que les valeurs associées : les valeurs brutes sont définies sur des valeurs préremplies lorsque vous définissez pour la première fois l'énumération dans votre code, comme les trois codes ASCII ci-dessus. La valeur brute d'un cas d'énumération particulier est toujours la même. Les valeurs associées sont définies lorsque vous créez une nouvelle constante ou variable basée sur l'un des cas de l'énumération et peuvent être différentes à chaque fois.

Valeurs brutes implicitement attribuées

Lorsque vous travaillez avec des énumérations qui stockent des valeurs brutes entières ou de chaîne, vous n'avez pas à affecter explicitement une valeur brute pour chaque cas. Si vous ne le faites pas, Swift attribue automatiquement les valeurs pour vous.

Par exemple, lorsque des entiers sont utilisés comme valeurs brutes, la valeur implicite de chaque cas est "+1" (on incrémente) par rapport au cas précédent. Si le premier cas n'a pas de valeur définie, sa valeur est 0 :


enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus
}

Dans l'exemple ci-dessus, Planet.mercury a une valeur brute explicite de 1, Planet.venus a une valeur brute implicite de 2, et ainsi de suite...

Lorsque des String sont utilisées pour des valeurs brutes, la valeur implicite de chaque observation (case) est le texte du nom de cette observation. Dans l'exemple ci-dessous, CompassPoint.south a une valeur brute implicite de "south", et ainsi de suite...


enum CompassPoint: String {
    case north, south, east, west
}

Vous accédez à la valeur brute d'un cas d'énumération avec sa propriété rawValue :


let earthsOrder = Planet.earth.rawValue
// earthsOrder : 3

let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection : "west"

Initialisation à partir d'une valeur brute

Si vous définissez une énumération avec un type à valeur brute, l'énumération reçoit automatiquement un initialiseur qui prend une valeur du type de la valeur brute (en tant que paramètre appelé rawValue) et renvoie un cas d'énumération ou nil. Vous pouvez utiliser cet initialiseur pour essayer de créer une nouvelle instance de l'énumération.

Cet exemple identifie Uranus à partir de sa valeur brute de 7 :


let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet is of type Planet? and equals Planet.uranus

Toutes les valeurs Int possibles ne trouveront pas une planète correspondante. Pour cette raison, l'initialiseur de valeur brute renvoie toujours un cas d'énumération optionnel ou facultatif. Dans l'exemple ci-dessus, possiblePlanet est de type Planet? ou "Planet optionnel".

Si vous essayez de trouver une planète avec une position de 11, la valeur Planet facultative renvoyée par l'initialiseur de valeur brute sera nil :


let positionToFind = 11

if let somePlanet = Planet(rawValue: positionToFind) {
    switch somePlanet {
        case .earth:
            print("Parfait pour y vivre")
        default:
            print("Pas sûr pour les humains")
    }
} else {
    print("Il n'y a pas de planète à la position \(positionToFind)")
}

// "Il n'y a pas de planète à la position 11"

Cet exemple utilise une liaison facultative pour essayer d'accéder à une planète avec une valeur brute de 11.

Énumérations récursives

Une énumération récursive est une énumération qui a une autre instance de l'énumération comme valeur associée pour un ou plusieurs des cas d'énumérations. Vous indiquez qu'un cas d'énumération est récursif en écrivant indirect avant lui.


enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

Vous pouvez aussi écrire indirect avant le début de l'énumération pour activer l'indirection pour tous les cas de l'énumération qui ont une valeur associée :


indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

Cette énumération peut stocker trois types d'expressions arithmétiques : un nombre brut, l'addition de deux expressions et la multiplication de deux expressions. Les observations (case) addition et multiplication ont des valeurs associées qui sont également des expressions arithmétiques - ces valeurs associées permettent d'imbriquer des expressions.


let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

Une fonction récursive est un moyen simple de travailler avec des données qui ont une structure récursive :


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))
// Affiche : "18"

Cette fonction évalue un nombre brut en renvoyant simplement la valeur associée. Il évalue une addition ou une multiplication en évaluant l'expression du côté gauche, en évaluant l'expression du côté droit, puis en les additionnant ou en les multipliant.