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 self
dans 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é (1
par 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.
Zéro Tech, le blog Tech de Zéro Janvier – @zerojanvier@diaspodon.fr