Découverte de Swift (7)

Après les propriétés vues dans le billet précédent, je continue mon tour d'horizon des fonctionnalités des classes, des structures et des énumérations dans Swift avec les méthodes.

Un premier point qui mérite d'être signalé avant de commencer est le fait que contrairement à d'autres languages qui réservent la définition de méthodes aux classes, Swift propose cette possibilité aux classes, aux structures et aux énumérations.

Méthodes d'instance

Une méthode d'instance est une méthode qui peut et doit être appelée pour une instance donnée d'une classe, d'une structure ou d'une énumération.

La syntaxe de défintion d'une méthode d'instance est la même que pour une fonction :

class Counter {
    var count = 0
    
    func increment() {
        count += 1
    }
    
    func increment(by amount: Int) {
        count += amount
    }
    
    func reset() {
        count = 0
    }
}

Il est ensuite possible d'appeler les méthodes pour une instance de cette classe :

let counter = Counter()
print(counter.count)    // Prints 0

counter.increment()
print(counter.count)    // Prints 1

counter.increment(by: 6)
print(counter.count)    // Prints 7

counter.reset()
print(counter.count)    // Prints 0

Rien de révolutionnaire pour le moment, la syntaxe est très classique et se lit très facilement.

La propriété 'self'

Chaque instance possède une propriété implicite self qui réfère à l'instance elle-même.

Si on reprend la méthode increment() de l'exemple précédent, il aurait été possible de l'écrire ainsi :

func increment() {
	self.count += 1
}

Dans beaucoup de cas, comme celui-ci, il n'est en réalité pas nécessaire de préciser self pour accéder à une propriété ou à une méthode de l'instance ; c'est implicitement le cas si on ne précise rien.

L'exception la plus courante est le cas où une méthode reçoit un paramètre ayant le même nom qu'une propriété. Pour lever toute ambiguïté, il faut alors utiliser self pour distinguer la propriété de l'instance du paramètre reçu dans la méthode.

On peut le voir en réécrivant une des méthodes increment de notre compteur :

func increment(by count: Int) {
	self.count += count
}

Comme la méthode reçoit un paramètre count ayant le même nom que la propriété de la classe, il faut préciser self.count pour mettre à jour la propriété à partir du paramètre count.

Modifier une structure ou une énumération dans une méthode d'instance

Contrairement aux classes qui sont des types par référence, les structures et les énumérations sont des types par valeur. Cela signifie que par défaut, c'est une copie de l'instance qui est passée à une méthode de cette instance, et il n'est alors pas possible de modifier les propriétés de cette instance.

Cela m'a semblé un peu contre-intuitif quand j'ai lu cela dans le livre, mais c'est assez logique quand on y repense.

Concrètement, si j'essaye de le faire, j'ai une erreur dès la compilation :

struct Point {
    var x = 0.0, y = 0.0
    
    func moveBy (x deltaX: Double, y DeltaY: Double) {
        x += deltaX		// /!\ ERREUR DE COMPILATION
        y += DeltaY		// /!\ ERREUR DE COMPILATION
    }
}
// Erreur de compilation sur les 2 lignes :
// "Left side of mutating operator isn't mutable: 'self' is immutable"

Pour contourner cette contrainte, il est possible de préciser qu'une méthode peut modifier les valeurs de l'instance en apposant le mot-clé mutating :

struct Point {
    var x = 0.0, y = 0.0
    
    mutating func moveBy (x deltaX: Double, y DeltaY: Double) {
        x += deltaX
        y += DeltaY
    }
}

Ensuite, non seulement cela compile mais il devient possible d'utiliser cette méthode pour déplacer notre Point :

var somePoint = Point(x: 1.0, y: 1.0)
print("The point coordinates are : (\(somePoint.x), \(somePoint.y))")
// Prints The point coordinates are : (1.0, 1.0)

somePoint.moveBy(x: 6, y: 2)
print("The point coordinates are : (\(somePoint.x), \(somePoint.y))")
// Prints The point coordinates are : (7.0, 3.0)

Par contre, il reste impossible de modifier une instance de structure déclarée comme constante :

let someOtherPoint = Point(x: 1.0, y: 1.0)
someOtherPoint.moveBy(x: 6, y: 2)	// /!\ ERREUR DE COMPILATION
// Erreur de compilation : "Cannot use mutating member on immutable value: 'someOtherPoint' is a 'let' constant"

Réassigner selfdans une méthode mutating

Dans jne méthode mutating, il est possible de réassigner une nouvelle instance à self : en sortie de la méthode, l'instance sur laquelle la méthode a été appelée est remplacée par la nouvelle instance qui vient d'être assignée.

Par exemple, dans notre méthode moveBy vue précédemment, il est possible de créer un nouveau Point avec les nouvelles coordonnées qui vient écraser l'instance actuelle, plutôt que de modifier les coordonnées de celle-ci :

mutating func moveBy (x deltaX: Double, y deltaY: Double) {
	self = Point(x: x + deltaX, y: y + deltaY)
}

self dans une énumération

Dans une énumération, une méthode mutating peut utiliser la propriété implicite self pour modifier la valeur (le case de l'énumération).

Prenons l'exemple d'une énumération qui modélise un interrupteur à trois positions : off, low et high :

enum TriStateSwitch {
    case off, low, high
    
    mutating func next() {
        switch self {
        case .off:
            self = .low
        case .low:
            self = .high
        case .high:
            self = .off
        }
    }
}

La méthode next() permet de basculer d'une position à l'autre en suivant un ordre prédéfini : off, puis low, puis high, retour à off, et ainsi de suite.

Il est ensuite aisé de tester le bon fonctionnement de notre interrupteur à 3 positions :

var mySwitch = TriStateSwitch.off

print(mySwitch) // Prints "off"

mySwitch.next()
print(mySwitch) // Prints "low"

mySwitch.next()
print(mySwitch) // Prints "high"

mySwitch.next()
print(mySwitch) // Prints "off"

Méthodes de type

De la même façon que pour les propriétés statiques, il est possible de définir des méthodes statiques, c'est-à-dire des méthodes qui portent le type lui-même, que ce soit une classe, une structure ou une énumération.

L'exemple suivant utilise cette fonctionnalité pour modéliser l'avancement des joueurs dans un jeu. Il jeu peut être joué par plusieurs joueurs, mais pas en même temps. De plus, hormis le premier niveau, tous les niveaux suivants sont verrouillés, jusqu'à ce qu'un des joueurs finisse le niveau précédent. Alors, le niveau est déverrouillé pour tous les joueurs.

Avec un tel mode de fonctionnement, il est pertinent de mettre en oeuvre au niveau statique la gestion du niveau maximal dévérouillé.

struct LevelTracker {
    
    static var highestUnlockedLevel = 1
    
    var currentLevel = 1
    
    static func unLock(_ level: Int) {
        if level > highestUnlockedLevel {
            highestUnlockedLevel = level
        }
    }
    
    static func isUnlocked(_ level: Int) -> Bool {
        return level <= highestUnlockedLevel
    }
    
    @discardableResult
    mutating func advance(to level: Int) -> Bool {
        if LevelTracker.isUnlocked(level) {
            currentLevel = level
            return true
        }
        else {
            return false
        }
    }
}

La structure LevelTracker possède : – une propriété statique highestUnlockedLevel contenant le plus haut niveau débloqué (1par défaut) – une propriété currentLevel contenant le niveau actuel – une méthode statique unLock pour débloquer un niveau – une méthode statique isUnlocked pour savoir si un niveau a été débloqué – une méthode d'instance advance pour essayer d'avancer à un niveau, le résultat peut être true si le niveau demandé est déjà débloqué ou false dans le cas contraire

On peut ensuite associer ce LevelTracker avec une classe modélisant un joueur :

class Player {
    var tracker = LevelTracker()
    let playerName: String
    
    func complete(level: Int) {
        LevelTracker.unLock(level + 1)
        tracker.advance(to: level + 1)
    }
    
    init(name: String) {
        playerName = name
    }
}

Cette classe Player contient une instance de LevelTracker pour gérer l'avancement du joueur dans le jeu.

Lorsque le joueur termine un niveau, on appelle la méthode complete qui réalise successivement deux actions : 1. appeler la méthode statique unLock de LevelTracker pour débloquer le niveau suivant (s'il n'est pas déjà débloqué) 2. avancer le tracker personnel du joueur au niveau suivant

Voyons cela en action :

var playerOne = Player(name: "Anna")
playerOne.complete(level: 6)
print("Current level for player \(playerOne.playerName) is \(playerOne.tracker.currentLevel)")
// Prints "Current level for player Anna is 7"
print("Highest unlocked level is \(LevelTracker.highestUnlockedLevel)")
// Prints "Highest unlocked level is 7"

var playerTwo = Player(name: "Bram")
playerTwo.complete(level: 2)
print("Current level for player \(playerTwo.playerName) is \(playerTwo.tracker.currentLevel)")
// Prints "Current level for player Bram is 3"
print("Highest unlocked level is \(LevelTracker.highestUnlockedLevel)")
// Prints "Highest unlocked level is 7"

Une première joueuse termine les 6 premiers niveaux : elle atteint donc le niveau 7, qui devient également le plus haut niveau débloqué.

Ensuite, un second joueur termine le niveau 2 : il atteint donc le niveau 3, et le plus haut niveau débloqué reste le niveau 7.

Si on essaye tout de même de faire avancer le second joueur au niveau 7, c'est autorisé car le premier joueur a déjà débloqué ce niveau :

if playerTwo.tracker.advance(to: 7) {
    print("Player \(playerTwo.playerName) is now at level 7")
}
else {
    print("Level 7 has not yet been unlocked")
}
// Prints "Player Bram is now at level 7"

Par contre, si on tente de le faire avancer jusqu'au niveau 10, il est bloqué :

if playerTwo.tracker.advance(to: 10) {
    print("Player \(playerTwo.playerName) is now at level 10")
}
else {
    print("Level 10 has not yet been unlocked")
}
// Prints "Level 10 has not yet been unlocked"

Et la suite ?

Ainsi s'achève ce billet et ce chapitre sur les méthodes, qui s'est avéré plus simple que celui sur les propriétés, à rebours de ce que j'attendais. Le prochain épisode, c'est un chapitre sur les subscripts, qui s'annonce plus court encore que celui-ci.

#swift

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