Découverte de Swift (10)

Suite de ma découverte de Swift avec un gros chapitre qui se penche sur le processus d'initialisation d'une instance, que ce soit d'une classe, d'une structure ou d'une énumération, à travers des méthodes un peu particulières : les initialisateurs.

Valeurs initiales d'une propriété stockée

Il existe une règle d'or pour les propriétés stockées d'une classe ou d'une structure : elles doivent toutes être initialisées lorsqu'une instance est créée. Une propriété stockée ne peut pas rester dans un état indéterminé.

L'initialisation de la valeur d'une propriété stockée peut se faire de 2 façons : – soit en lui assignant une valeur par défaut lors de sa définition – soit en lui assignant une valeur dans un initialisateur

Initialisateur

Un initialisateur est une méthode spéciale qui porte le nom réservé init, avec ou sans paramètres.

Dans un cas simple, avec un initialisateur sans aucun paramètre, nous pouvons avoir quelque chose d'aussi simple que cela :

struct Fahrenheit {
    
    var temperature:Double
    
    init() {
        temperature = 32.0
    }
}

var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
// Prints "The default temperature is 32.0° Fahrenheit"

C'est simple comme bonjour : la création de l'instance fde la structure Fahrenheitappelle automatiquement l'initialisateur init() qui assigne une température par défaut de 32.0.

Valeur par défaut

Autre approche : assigner directement une valeur par défaut lors de la déclaration de la propriété stockée.

Nous pouvons par exemple réécrire notre structure Fahrenheitde façon simplifiée, avec le même résultat que dans la version précédente qui utilisait un initialisateur :

struct Fahrenheit {
    var temperature:Double = 32.0
}

var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
// Prints "The default temperature is 32.0° Fahrenheit"

Aller plus loin dans l'initialisation

Le processus d'initialisation ne s'arrête pas là, il offre d'autres possibilités plus riches que celles abordées dans cette première partie. La suite va présenter quelques exemples de ces possiblités.

Paramètres d'initialisation

Un initialisateur peut recevoir des paramètres, de la mêle façon que le fait une méthode classique.

En restant dans notre exemple avec des températures, nous pouvons créer une nouvelle structure pour gérer des températures en degrés Celcius et pouvant être initialisée à partir d'une température en degrés Fahrenheit ou Kelvin :

struct Celsius {
    
    var temperatureInCelcius: Double
    
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelcius = (fahrenheit - 32.0) / 1.8
    }
    
    init(fromKelvin kelvin: Double) {
        temperatureInCelcius = kelvin - 273.15
    }
}

Nous avons créé 2 initialisateurs, l'un recevant en paramètre une température en degrés Fahrenheit, l'autre en degrés Kelvin, avec pour chacun une formule différente pour initialiser la température en degrés Celcius.

Il est alors simple de créer 2 instances différentes de Celcius, l'une à partir d'une température en degrés Fahrenheit, l'autre en degrés Kelvin, et de constater que l'initialisation et la conversion fonctionnenent bien dans les deux cas :

var boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
print("Boiling point of water is \(boilingPointOfWater.temperatureInCelcius)°C")
// Prints "Boiling point of water is 100.0°C"

let freezingPointOfWater = Celsius(fromKelvin: 273.15)
print("Freezing point of water is \(freezingPointOfWater.temperatureInCelcius)°C")
// Prints "Freezing point of water is 0.0°C"

Noms des paramètres d'initialisation

Comme pour les paramètres des méthodes et des fonctions, les paramètres d'initialisation peuvent avoir à la fois un nom de paramètre, utilisé à l'intérieur de l'initialisateur, et une étiquette ou nom externe, utilisé lors de l'appel de l'initialisateur.

Swift affecte automatiquement une étiquette à chaque paramètre d'un initialisateur : si l'étiquette n'est pas déclarée explicitement, il s'agit du nom du paramètre lui-même.

Cela permet notamment de savoir quel initialisateur doit être utilisé lorsque qu'il en existe plusieurs et qu'il pourrait y avoir une ambiguïté si par exemple deux initialisateurs possèdent le même nombre de paramètres.

Par exemple, imaginons une structure pour modéliser une couleur avec trois propriétés reflétant sa composition en rouge, vert et bleu :

struct Color {
    
    let red, green, blue: Double
    
    init(red: Double, green: Double, blue: Double) {
        self.red = red
        self.green = green
        self.blue = blue
    }
    
    init(white: Double) {
        red = white
        green = white
        blue = white
    }
}

Nous pouvons utiliser l'un des deux initialisateurs : – le premier reçoit en paramètre les valeurs des composants red, greenet blue – le second reçoit un seul paramètre qui contient la valeur qui doit être assignée aux 3 composants

En pratique, cela donne cela :

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)

D'un côté, une couleur magenta composée de rouge et de bleu, sans vert. De l'autre, une nuance de gris où chaque composant est rempli à 50%.

Il est toutefois possible de déclarer explicitement un paramètre d'initialisation sans étiquette, en utilisant _ comme étiquette. Par exemple, si on veut créer un initialisateur simple pour notre structure Celsius:

init(_ celsius: Double) {
        temperatureInCelcius = celsius
    }

On peut ensuite créer très facilement une instance de Celsiusavec cet initialisateur simplifié :

let bodyTemperature = Celsius(37.2)
print("Body temperature is \(bodyTemperature.temperatureInCelcius)°C")
// Prints "Body temperature is 37.2°C"

Propriétés sans valeur : optional

Si une propriété stockée doit être autorisée à ne pas avoir de valeur, il est possible de déclarer de type optional. Par défaut, elle sera alors initialisée avec la valeur nil et il devient possible de ne pas lui assigner de valeur définie lors de l'initialisation d'une instance.

Imaginons par exemple une classe qui doit modéliser une question d'un sondage :

class SurveyQuestion {
    
    var question: String
    var answer: String?
    
    init(question: String) {
        self.question = question
    }
    
    func ask() {
        print(question)
    }
}

let cheeseQuestion = SurveyQuestion(question: "Do you like cheese ?")
cheeseQuestion.ask()
// Prints "Do you like cheese ?"
cheeseQuestion.answer = "Yes, I do like cheese."

Cette classe contient 2 propriétés stockées : – une question de type String, qui doit obligatoirement être alimentée lors de l'initialisation d'une instance de la classe – une réponse de type String?, c'est-à-dire un optional de type Stringqui contiendra la réponse lorsqu'elle sera connue mais qui peut rester indéterminée dans un premier temps

L'initialisateur se contente donc d'alimenter la question, sans initialiser la réponse. Cette réponse peut ensuite être alimentée lorsque c'est nécessaire.

Initialisation d'une propriété constante

Une propriété constante peut être initialisée à n'importe quel moment de l'initialisation, la seule contrainte étant que sa valeur ne peut plus être modifiée une fois qu'elle a été assignée. Une fois qu'une valeur a été assignée, elle ne peut plus être modifiée.

Dans notre exemple de questionnaire pour un sondage, on peut imaginer que la question est constante (elle n'a pas vocation à être modifiée une fois que le sondage est créé) et ainsi réécrire notre classe de la façon suivante :

class SurveyQuestion {
    
    let question: String
    var answer: String?
    
    init(question: String) {
        self.question = question
    }
    
    func ask() {
        print(question)
    }
}

let cheeseQuestion = SurveyQuestion(question: "How about chocolate ?")
cheeseQuestion.ask()
// Prints "Do you like cheese ?"
cheeseQuestion.answer = "I also like chocolate, but not with cheese."

Bien que la propriété questionsoit désormais une constante, elle n'a pas de valeur par défaut dans sa déclaration. Par contre, elle est obligatoirement alimentée dans l'initialisateur, et ne peut plus être modifiée ensuite.

Initialisateurs par défaut

Dans une classe ou une structure dont toutes les propriétés ont une valeur par défaut et qui ne déclare pas explicitement un initialisateur, Swift propose un initialisateur par défaut. Celui-ci crée simplement une instance de la classe ou de la structure avec les valeurs par défaut prévues pour chaque propriété.

Par exemple, avec une classe simple pour modéliser une liste de courses :

class ShoppingListItem {
    var name: String?
    var quantity = 1
    var purchased = false
}

var item = ShoppingListItem()
print("\(item.name) / \(item.quantity) / \(item.purchased)")
// Prints "nil / 1 / false"

Il est possible de créer un article de notre liste de courses avec les valeurs par défaut prévues. Notons que dans le cas de la propriété namequi est un optional, la valeur par défaut utilisée par l'initialisateur par défaut est nil.

Initialisateurs par défaut pour une structure

Swift va encore plus loin pour les structures : si une structure ne déclare aucun initialisateur, un initialisateur par défaut est créé automatiquement avec les propriétés stockées de la structure. Il est même possible de passer en paramètre certaines propriétés et pas d'autres.

Prenons l'exemple d'une structure Sizeavec 2 propriétés pour gérer la largeur et la hauteur :

struct Size {
    var width = 0.0, height = 0.0
}

let twoByTwo = Size(width: 2.0, height: 2.0)
print("twoByTwo : \(twoByTwo.width) * \(twoByTwo.height)")
// Prints "twoByTwo : 2.0 * 2.0"

let zeroByTwo = Size(height: 2.0)
print("zeroByTwo : \(zeroByTwo.width) * \(zeroByTwo.height)")
// Prints "zeroByTwo : 0.0 * 2.0"

let twoByZero = Size(width: 2.0)
print("twoByZero : \(twoByZero.width) * \(twoByZero.height)")
// Prints "twoByZero : 2.0 * 0.0"

L'initialisateur par défaut créé par Swift permet de créer une instance Sizeavec la largeur et la hauteur ou seulement l'un de ces deux paramètres. Si l'un des paramètres n'est pas renseigné, c'est sa valeur par défaut qui est utilisée pour initialiser l'instance.

Délégation à un autre initialisateur

Un initialisateur peut déléguer une partie de ses opérations à un autre initialisateur. Cela évite de dupliquer le même code dans plusieurs initialisateurs.

Le fonctionnement est différent pour les types par valeur, comme les structures, et pour les types par référence, comme les classes. Pour les classes, la notion d'héritage complexifie la gestion de la délégation, ce sera l'objet d'un autre chapitre. Nous allons nous concentrer ici sur le cas plus simple des structures.

Dans un initialisateur, et seulement dans un initialisateur, il est possible d'appeler un autre initialisateur avec le syntaxe self.init() en fournissant les paramètres adéquats.

Prenons un exemple concret pour voir comment cela fonctionne :

struct Rectangle {
    
    var origin = Point()
    var size = Size()
    
    var description: String {
        return "Origin : (\(origin.x), \(origin.y)) - Size : (\(size.width), \(size.height))"
    }
    
    init() {}
    
    init(origin: Point, size: Size) {
        self.origin = origin
        self.size = size
    }
    
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        let originPoint = Point(x: originX, y: originY)
        self.init(origin: originPoint, size: size)
    }
    
}

Nous avons créé une structure Rectangle avec : – deux propriétés stockées de type Point et Size pour modéliser le point d'origine et la taille du rectangle – une propriété calculée pour obtenir une description synthétique du rectangle – un initialisateur par défaut qui ne fait rien explicitement (implicitement, les valeurs par défaut des propriétés stockées sont utilisées) – un initialisateur qui reçoit le point d'origine et la taille du rectangle – un initialisateur qui reçoit le point au centre du rectangle ainsi que sa taille, en déduit le point d'origine, et appelle l'initialisateur précédent avec le point d'origine calculée et la taille

On peut ensuite voir en action les 3 initialisateurs différents :

let basicRectangle = Rectangle()
print(basicRectangle.description)
// Prints "Origin : (0.0, 0.0) - Size : (0.0, 0.0)"

let originRectangle = Rectangle(origin: Point(x:2.0, y: 2.0),
                                size: Size(width: 5.0, height: 5.0))
print(originRectangle.description)
// Prints "Origin : (2.0, 2.0) - Size : (5.0, 5.0)"

let centerRectangle = Rectangle(center: Point(x: 4.0, y: 4.0),
                                size: Size(width: 3.0, height: 3.0))
print(centerRectangle.description)
// Prints "Origin : (2.5, 2.5) - Size : (3.0, 3.0)"

Chaque initialisateur fonctionne comme attendu, avec en particulier centerRectanglequi est initialisé à partir de son point central et dont l'initialisateur a calculé le point d'origine avant de faire appel à l'initialisateur adéquat.

Et la suite ?

Le chapitre sur l'initialisation étant particulièrement long, j'ai choisi d'arrêter ici ce billet. Le prochain reprendra le chapitre sur l'initialisation, avec notamment une partie particulièrement dense sur le mélange de l'initialisation et de l'héritage pour les classes.

#swift

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