Découverte de Swift (14)

Suite de ma découverte de Swift avec un quatorzième épisode consacré à l'optional chaining.

Commençons par définir l'optional chaining : il s'agit d'un processus qui permet de faire appel à des propriétés, des méthodes ou des subscripts sur un optional qui peut être nil.

Si l'optional contient une valeur, l'appel à la propriété, la méthode ou le subscript réussit ; par contre, l'optional est nil, l'appel à la propriété, la méthode ou le subscript renvoie nil.

Plusieurs appels peuvent ainsi être enchainés, et la chaine ainsi formée peut échouer en renvoyant nilquel que soit le maillon qui échoue.

Optional Chaining comme alternative à l'unwrapping forcé

Pour rappel, l'unwrapping forcé consiste à placer un point d'exclamation ! après le nom d'un optional pour forcer l'interprétation de sa valeur en posant l'hypothèse que l'optional contient bien une valeur. Si l'optional est nil, on obtient une erreur à l'exécution.

L'optional chaining se déclare en plaçant un point d'interrogation ? après l'appel d'une propriété, d'une méthode ou d'un subscript, et renvoie un optional du type normalemetn renvoyé par cet appel. Par exemple, l'optional chaining de l'appel d'une méthode qui renvoie normalement un Int renvoie un Int?.

Un exemple simple :

class Residence {
    var numberOfRooms = 1
}

class Person {
    var residence: Residence?
}

let john = Person()
let roomCount = john.residence!.numberOfRooms
// Fatal error: Unexpectedly found nil while unwrapping an Optional value

Nous avons une classe simple Residence avec un nombre de pièces, une classe Person qui définit une propriété optional de type Residence.

En créant une instance par défaut de la classe Person, la propriété residence garde sa valeur par défaut : nil pour un optional. Quand on essaye de forcer l'unwrapping de cet optional alors qu'il n'est pas initialisé, on tombe sur une erreur d'exécution.

En utilisant l'optional chaining par le suffixe ?, on peut gérer les deux cas, selon que la résidence soit connue ou non :

let john = Person()
if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
}
else {
    print("Unable to retrieve the number of rooms.")
}
// Prints "Unable to retrieve the number of rooms."

Si maintenant on affecte à John une résidence par défaut, l'optional chaining récupère bien le nombre de pièces de cette résidence :

let john = Person()
john.residence = Residence()
if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
}
else {
    print("Unable to retrieve the number of rooms.")
}
// Prints "John's residence has 1 room(s)."

Un modèle de classes pour l'optional chaining

Nous allons maintenant étendre nos classes créées précédemment.

La classe Person reste la même :

class Person {
    var residence: Residence?
}

Nous créons une nouvelle classe Room qui modélise une pièce, avec son nom :

class Room {
    let name: String
    init(name: String) { self.name = name }
}

Nous adaptons alors la classe Residence pour gérer de façon plus précise les pièces d'une résidence :

class Residence {
    
    var rooms = [Room]()
    
    var numberOfRooms: Int {
        return rooms.count
    }
    
    subscript(i: Int) -> Room {
        get {
            return rooms[i]
        }
        set {
            rooms[i] = newValue
        }
    }
    
    func printNumberofRooms() {
        print("The number of rooms is \(numberOfRooms).")
    }
}

Nous créons enfin une nouvelle classe Address :

class Address {
    
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    
    func buildingIdentifier() -> String? {
        if let buildingNumber = buildingNumber, let street = street {
            return "\(buildingNumber) \(street)"
        }
        else if buildingName != nil {
            return buildingName
        }
        else {
            return nil
        }
    }
}

L'adresse est modélisée avec 3 propriétés stockées optionnelles : le nom de l'immeuble, le numéro et la rue. Une méthode permet de renvoyer l'identifiant de l'adresse, en fonction des données renseignées. Cette méthode renvoie un optional car si aucune donnée n'est renseignée dans l'adresse, la méthode renvoie nil.

Accès à des propriétés par l'optional chaining

Avec ce nouveau modèle de classes, nous pouvons reprendre notre premier exemple, avec John et sa résidence inconnue :

let john = Person()
if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) rooms.")
}
else {
    print("Unable to retrieve the number of rooms.")
}
// Prints "Unable to retrieve the number of rooms."

Nous pouvons même essayer d'ajouter une adresse à la résidence inconnue de John :

let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress

Cela ne fonctionne pas car l'optional chaining de residence échoue et renvoie nil, ce qui empêche évidemment d'affecter une adresse à cette résidence inconnue. On peut s'en assurer avec une petite fonction de test :

func createAddress() -> Address {
    print("Function was called.")
    let someAddress = Address()
    someAddress.buildingNumber = "29"
    someAddress.street = "Acacia Road"
    return someAddress
}

john.residence?.address = createAddress()
// Prints nothing

On essaye d'ajouter l'adresse de la résidence de John avec le résultat de la fonction createAddress : on se rend compte que celle-ci n'est pas appelée car sinon elle afficherait le message Function was called.

Accès à des méthodes par l'optional chaining

Dans la classe Residence, nous avions défini une méthode pour afficher le nombre de pièces de la résidence :

func printNumberofRooms() {
        print("The number of rooms is \(numberOfRooms).")
    }

Cette méthode ne renvoie aucune valeur explicitement : son type de retour est donc Void. Si on l'appelle par optional chaining, le retour sera donc de type Void?. Cela permet de tester le retour de l'appel :

if john.residence?.printNumberofRooms() != nil {
    print ("It was possible to print the number of rooms.")
}
else {
    print ("It was NOT possible to print the number of rooms.")
}
// Prints "It was NOT possible to print the number of rooms."

Cela fonctionne pareil pour l'utilisateur du set d'une propriété, qui est finalement une méthode qui renvoie implicitement une valeur de type Void (ou Void? par optional chaining):

if (john.residence?.address = someAddress) != nil {
    print("It was possible to set the address.")
}
else {
    print("It was NOT possible to set the address.")
}
// Prints "It was NOT possible to set the address."

Accès à un subscript par optional chaining

Comme pour les propriétés et les méthoses, il est possible d'utiliser un subscript par optional chaining sur un optional, comme notre liste de pièces d'une résidence :

if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
}
else {
    print("Unable to retrieve the first room name")
}
// Prints "Unable to retrieve the first room name"

Il faut noter que le suffixe ? est utilisé juste après residenceet avant l'index [0] car c'est bien la propriété residence qui est un optional et donc potentiellement nil.

Si maintenant nous créons enfin la résidence de John, il devient possible d'accéder à la lpremière pièce par subscript :

let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Living Room"))
johnsHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
}
else {
    print("Unable to retrieve the first room name")
}
// Prints "The first room name is Living Room."

Empiler plusieurs niveaux d'optional chaining

La magie de l'optional chaining, c'est que l'on peut empiler ainsi plusieurs appels.

Par contre, l'empilement d'optional chaining n'ajoute qu'un seul niveau d'optional : – si le type que l'on essaye de récupérer n'est pas optional, il devient optional par l'optional chaining – si le type que l'on essaye de récupérer est déjà optional, il le reste mais ne devient pas plus optional pour autant

Ainsi : – l'optional chaining d'un Int devient un Int? – l'optional chaining d'un Int? reste un Int?

En pratique :

if let johnsStreet = john.residence?.address?.street {
    print("John's street name is \(johnsStreet).")
}
else {
    print("Unable to retrieve the address")
}
// Prints "Unable to retrieve the address"

L'optional chaining échoue et renvoie nil car bien que la résidence soit désormais initialisée, l'adresse de cette résidence reste inconnue.

Si on définit cette adresse, l'optional chaining fonctionne désormais :

let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence?.address = johnsAddress

if let johnsStreet = john.residence?.address?.street {
    print("John's street name is \(johnsStreet).")
}
else {
    print("Unable to retrieve the address")
}
// Prints "John's street name is Laurel Street."

Et la suite ?

Ce chapitre était relativement court mais pointu, j'ai volontairement omis quelques spécificités que je n'avais pas le courage de détailler ici. Le prochain chapitre, et le billet correspond, sera consacré à la gestion des erreurs : tout un programme !

#swift

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