De bien belles boucles avec Swift

Aujourd'hui nous allons voir plusieurs façons de boucler dans une structure de données en Swift : une méthode héritée du C, une méthode qui rappelle Ruby, et une autre façon Programmation Fonctionnelle.

Nous verrons des exemples avec tableaux (Array) et dictionnaires (Dictionary), et en profiterons pour visiter les “tuples”.

Article mis-à-jour le 2015-06-11.

Les exemples sont écrits pour Swift 1.2 mais des mises-à-jour pour Swift 2.0 sont présentes à la fin de certains chapitres du tutoriel.

Les boucles à l'ancienne

Il peut être tentant de conserver ses habitudes et de ne pas évoluer. Après tout, si ça marche, pourquoi changer ?

Par exemple, voici une boucle “à l'ancienne”, qui ressemble à du C (ou de l'Objective-C), et qui itère dans un tableau nommé “players” :

let players = ["Joe", "James", "Jack"]
for var i = 0; i < players.count; i++ {
    println("Player is '\(players[i])'")
}

Résultat :

Player is 'Joe'
Player is 'James'
Player is 'Jack'
  1. On déclare une variable i qui démarre à zéro
  2. On donne comme limite le nombre d'objets dans le tableau players
  3. On incrémente la variable à chaque tour
  4. On utilise i comme index pour notre objet

Et si l'on veut utiliser l'index du joueur dans le tableau?

let players = ["Joe", "James", "Jack"]
for var i = 0; i < players.count; i++ {
    println("Player \(i + 1) is '\(players[i])'")
}

Résultat :

Player 1 is 'Joe'
Player 2 is 'James'
Player 3 is 'Jack'

Bon, ça marche… mais c'est laid, c'est facile de se tromper en tapant, et c'est très limité en possibilités. Voyons comment faire mieux.

Les boucles modernes

Comme en Ruby, nous pouvons utiliser en Swift une manière plus moderne de faire une boucle, en itérant directement dans le tableau sans variable d'index.

for player in players {
    println("Player is '\(player)'")
}

Résultat :

Player is 'Joe'
Player is 'James'
Player is 'Jack'

Ici player est une variable qui représente l'objet en cours, dans le tableau, à chaque tour de boucle. J'aurais pu la nommer autrement, mais la convention est d'utiliser pluriel pour le tableau d'objets et singulier pour l'objet lui-même.

Bon, c'est en effet bien mignon, mais si je veux utiliser l'index du joueur dans le tableau, comment je fais?

On va utiliser la fonction enumerate qui fait partie de la bibliothèque standard de Swift: appliquée à un tableau, cette fonction retourne, en plus de l'objet, l'index de l'objet.

enumerate est un peu particulier puisque pour chaque itération il retourne non pas une valeur mais deux, le tout encapsulé dans un “tuple”.

Mais voyons par l'exemple :

for player in enumerate(players) {
    println("Player \(player.0 + 1) is '\(player.1)'")
}

Résultat :

Player 1 is 'Joe'
Player 2 is 'James'
Player 3 is 'Jack'

Dans cette boucle, à chaque itération, la variable player est un tuple contenant deux valeurs : l'index et le nom.

Exemple : lors du premier tour, player est égal à (0, "Joe"). Ensuite, au deuxième tour, player est égal à (1, "James"), puis au troisième tour player est égal à (2, "Jack").

Le tuple player contient deux valeurs : pour y accéder, on utilise… l'index des valeurs. La première est donc d'index 0, et la seconde d'index 1. On utilise la notation objet.index comme ceci : player.O et player.1.

Attention à ne pas confondre l'index de la valeur dans le tuple avec la valeur numérique de l'objet placé à cet index…

let tup = ("Yes", "No", "Maybe")

Ici le tuple “tup” contient trois valeurs. Pour récupérer “Yes”, on fait tup.0, pour récupérer “No” on fait tup.1 et enfin pour récupérer “Maybe” on fait tup.2.

Ok, c'est un peu lourd ces index de tuples…

Heureusement, on peut utiliser des named tuples, c'est-à-dire qu'on peut attribuer des noms de variables aux objets qui sont dans le tuple, au lieu d'y accéder avec les index :

for (index, name) in enumerate(players) {
    println("Player \(index + 1) is '\(name)'")
}

Résultat :

Player 1 is 'Joe'
Player 2 is 'James'
Player 3 is 'Jack'

Pas mal, ça ! On retrouve un peu de souplesse ! C'est d'ailleurs cette méthode qui est considérée comme “standard” en Swift. Mais il y a encore une autre façon de s'y prendre…


Mise-à-jour pour Swift 2:

A partir de Swift 2 la plupart des fonctions globales de type enumerate sont devenues des méthodes accessibles sur l'objet lui-même.

Dans notre cas, celà signifie qu'avec Swift 2 (apparu dans Xcode 7 beta) la boucle devient :

for (index, name) in players.enumerate() {
    print("Player \(index + 1) is '\(name)'")
}

Vous avez remarqué par ailleurs que println a disparu, seul subsiste print qui prend désormais par défaut le comportement de println.


Sans boucles !

Oui, on peut faire une itération sur un tableau d'éléments en Swift sans utiliser de boucle !

On utilisera pour ça map, faisant partie de la bibliothèque standard. C'est une fonction qui travaille sur un tableau et qui est capable d'appliquer une transformation à chaque élément avant de le retourner.

Exemple tout simple de la fonction map:

let names = players.map({ $0.uppercaseString })
println(names) // prints "[JOE, JAMES, JACK]"

Dans cet exemple nous avons notre tableau de players qui est ["Joe", "James", "Jack"].

Nous utilisons map pour passer chaque nom en majuscule avec la fonction uppercaseString, et $0 représente l'objet du tableau en cours de traitement.

Voici le même exemple mais sans le raccourci $0 et avec une variable de notre choix à la place :

let names = players.map({ name in name.uppercaseString })

Bon, maintenant qu'on a vu à quoi servait map, voyons comment on peut l'utiliser pour notre tableau “players” :

map(enumerate(players)) { (index, player) in println( "Player \(index + 1) is '\(player)'") }

On passe enumerate(players) à map, qui pour chaque élément du tableau va considérer un tuple (index, player), et exécuter notre fonction println en utilisant les variables nommées de ces tuples.


Mise-à-jour pour Swift 2:

players.enumerate().map { (index, player) in print( "Player \(index + 1) is '\(player)'") }

Dictionnaires

Nous avons vu trois manières de boucler dans un tableau. Mais comment faire pour un dictionnaire ?

Pour rappel, un tableau est une structure indexée et donc ordonnée d'éléments ; un dictionnaire est une structure non indexée (non ordonnée) de paires clé/valeur.

let players = ["Joe", "James", "Jack"] // tableau
let scores = ["Joe": 33, "James": 42, "Jack": 1000] // dictionnaire

Alors que dans le tableau “Joe” occupe officiellement la première position, dans le dictionnaire c'est une illusion, il n'a pas d'index.

En effet, ["Joe": 33, "James": 42, "Jack": 1000] est parfaitement égal à ["Jack": 1000, "Joe": 33, "James": 42], les deux dictionnaires contiennent les mêmes informations.

Commençons par voir que l'on peut boucler dans un dictionnaire très simplement comme dans un tableau, à la différence qu'à chaque itération l'élément sera un tuple contenant la paire clé/valeur :

let scores = ["Joe": 33, "James": 42, "Jack": 1000]
for score in scores {
    println("\(score.0) has \(score.1) points")
}

Résultat :

Joe has 33 points
James has 42 points
Jack has 1000 points

Mais c'est bien plus lisible et pratique si l'on utilise un “named tuple”, un tuple avec des noms de variables :

for (name, score) in scores {
    println("\(name) has \(score) points")
}

Et c'est donc la méthode standard pour boucler dans un dictionnaire avec Swift.

Bon, c'est cool, mais… si je veux utiliser l'index du joueur dans le dico?

Attention, on a dit qu'un dico n'avait pas d'index !

Mais on peut s'en servir quand même pour créer une liste numérotée ; il faut simplement garder à l'esprit que contrairement aux indexes d'un tableau, ce ne sont que des numéros générés sur le moment, pas leur véritable attribut:

for (index, score) in enumerate(scores) {
    println("Player n°\(index + 1): \(score.0) has \(score.1) points")
}

Résultat :

Player n°1: Joe has 33 points
Player n°2: James has 42 points
Player n°3: Jack has 1000 points

Et avec un “named tuple” :

for (index, (name, score)) in enumerate(scores) {
    println("Player n°\(index + 1): \(name) has \(score) points")
}

Ici on utilise un tuple dans un tuple, inception ! Mais pourtant c'est bien la méthode la plus facile à utiliser et à lire.

Allez, bonus, pour finir, boucler dans un dico sans faire de boucle, avec map :

map(enumerate(scores)) { (index, score) in println( "Player n°\(index + 1): \(score.0) has \(score.1) points") }

Mise-à-jour pour Swift 2:

Avec Swift 2, les deux derniers exemples deviennent respectivement :

for (index, (name, score)) in scores.enumerate() {
    print("Player n°\(index + 1): \(name) has \(score) points")
}

et

scores.enumerate().map { (index, score) in print( "Player n°\(index + 1): \(score.0) has \(score.1) points") }

Auteur: Eric Dejonckheere