III. Structures de données▲
La plus grande force de Ruby réside dans ses structures de données. Ce chapitre vous enseignera ces concepts pour écrire des programmes bien plus intéressants. Vous allez apprendre comment exprimer facilement des informations complexes, comme un carnet d'adresses complet. Ce chapitre est indispensable si vous désirez créer des programmes plus ou moins complexes.
III-A. Tableaux : introduction▲
Vous vous êtes déjà familiarisé avec deux classes Ruby, Integer et String. La classe Array représente une collection d'objets. Voici un exemple :
La méthode class nous indique que la variable nombres est un Array (plus précisément, un objet provenant de la classe Array). Vous pouvez accéder aux éléments du tableau de cette façon :
Vous pouvez rajouter des éléments dans le tableau tout simplement en tapant :
Remarquez que les éléments d'un tableau sont enregistrés séquentiellement. Un tableau peut contenir autant d'objets que vous le désirez. Les objets contenus dans le tableau peuvent être manipulés comme nous venons de voir :
Un tableau commence à compter à 0, pas 1 ! Ainsi, nombres[1] nous donnera le deuxième élément du vecteur, et non pas le premier.
Quels types de choses pouvons-nous insérer dans un tableau ? N'importe quel objet, en fait. Par exemple, des entiers et des chaines de caractères :
Et pourquoi pas un autre tableau ?
Vous comprenez pourquoi les tableaux sont vraiment intéressants ?
III-B. Que peuvent faire les tableaux?▲
Nous allons découvrir dans cette section quelques méthodes utiles concernant les tableaux en Ruby.
III-B-1. Array#sort▲
Vous pouvez trier les éléments d'un tableau en utilisant la méthode Array#sort :
III-B-2. Array#reverse▲
Inverser les éléments d'un tableau se fait tout aussi facilement :
III-B-3. Array#length▲
La méthode Array#length vous donne le nombre d'éléments d'un tableau :
III-B-4. Opérations arithmétiques sur des tableaux▲
Les méthodes Array#+, Array#- et Array#* se comportent logiquement :
Il n'existe évidemment pas de méthode Array#/, vu qu'il est impossible de diviser un tableau.
Souvenez-vous des raccourcis +=, -= et *=. Ils fonctionnent également avec des tableaux.
III-B-5. Afficher le contenu d'un tableau▲
Finalement, nous pouvons bien sûr afficher le contenu d'un tableau à l'écran, en utilisant l'instruction puts :
Souvenez-vous, nil signifie que puts ne renvoie rien.
Notez également qu'il est possible de convertir un tableau en une chaine de caractères, en invoquant la méthode Array#to_s :
III-B-6. Quelques exercices▲
Selon vous, que fera ce bout de code ?
>>adresses = [
[ 17, "Boulevard de la Sauvenière" ],
[ 2, "Place de la République Française"],
[ 19, "Rue de la Renaissance"]
]
>>adresses.sort
Et ceci?
>>adresses = [
[ 20, "Rue de la Renaissance" ],
[ 20, "Place de la République Française"]
]
>>adresses.sort
N'oubliez pas votre ami IRB !
III-C. Itérateurs▲
Cette section illustre une des fonctionnalités les plus intéressantes de Ruby : les itérateurs.
Un itérateur est une méthode un peu particulière. Il s'agit d'une méthode vous permettant d'accéder un par un à des éléments.
Les tableaux font partie des objets capables de supporter des itérateurs. Voici un exemple tout simple, utilisant la méthode Array#each :
amis = [ "Benjamin", "David", "Stéfanie", "Laura" ]
amis.each do |ami|
puts "J'ai un ami qui s'appelle " + ami
end
Ce qui produira à l'écran :
J'ai un ami qui s'appelle Benjamin
J'ai un ami qui s'appelle David
J'ai un ami qui s'appelle Stéfanie
J'ai un ami qui s'appelle Laura
Le lecteur attentif aura peut-être remarqué que cette méthode d'itération ressemble fort à la toute première boucle illustrée dans ce tutoriel (celle avec la forme n.times do … end). Il s'agit effectivement d'un itérateur! Cette méthode vous permet d'itérer sur la séquence d'entiers compris entre 0 et n-1. Regardez :
4.times do |nombre|
puts nombre
end
Ce qui produira à l'écran :
0
1
2
3
Vous remarquerez que l'itérateur times commence à compter à 0. Un peu comme les indices des tableaux (0 représentant le premier élément). Donc, le code suivant :
amis = [ "Benjamin", "David", "Stéfanie", "Laura" ]
amis.each do |ami|
puts "J'ai un ami qui s'appelle " + ami
end
Peut également être réécrit comme ceci :
# 'i' est une notation standard pour représenter un indice.
4.times do |i|
puts "J'ai un ami qui s'appelle " + amis[i]
end
Maintenant, voici quelque chose d'amusant. Vous souvenez-vous de la méthode Array#length? Nous pouvons améliorer notre code en l'utilisant :
amis.length.times do |i| # 'i' pour indice
puts "J'ai un ami qui s'appelle " + amis[i]
end
Essayons maintenant d'afficher uniquement les éléments du tableau dont l'indice est pair. Le moyen le plus facile de déterminer si un entier est pair est de vérifier si le reste de sa division avec 2 est bien égal à 0. Souvenez-vous, l'opérateur Integer#% donne le reste d'une division. Voici le code :
amis.length.times do |i| # 'i' pour indice
# Nous n'affichons que les indices pairs
if i % 2 == 0
puts "J'ai un ami qui s'appelle " + amis[i]
end
end
Ce qui produira :
J'ai un ami qui s'appelle Benjamin
J'ai un ami qui s'appelle Stéfanie
Comment faire pour afficher les amis dans l'ordre alphabétique? Rien de plus simple, il suffit d'utiliser la méthode Array#sort :
amis.sort.each do |ami|
puts "J'ai un ami qui s'appelle " + ami
end
Ce qui produira :
J'ai un ami qui s'appelle Benjamin
J'ai un ami qui s'appelle David
J'ai un ami qui s'appelle Laura
J'ai un ami qui s'appelle Stéfanie
III-C-1. Quelques exercices▲
- Affichez le contenu du tableau amis dans l'ordre inverse alphabétique.
- Vous souvenez-vous de l'exercice du chapitre 2, où vous deviez afficher à l'écran les paroles de la chanson «Un kilomètre à pied, ça use…» ? Réimplémentez votre solution, en utilisant cette fois-ci un itérateur!
- Considérez le tableau suivant :
noms = [ "laurent", "david", "stéfanie", "laura" ]
Écrivez à l'écran la liste de ces noms, avec une majuscule comme première lettre. La méthode à utiliser est String#capitalize.
III-D. Hachages▲
Nous allons étudier dans cette section la classe Hash.
Les hachages ne sont rien d'autre que des tableaux spécialisés. Au lieu de n'accepter que des indices représentés par des nombres entiers (comme dans mon_tableau[3]), les hachages acceptent n'importe quel objet comme «index». Par exemple, vous pouvez écrire mon_hachage[« une_chaine_de_caractères »].
Supposons que nous voulons enregistrer des informations concernant un ami. Nous pourrions utiliser un tableau, comme ceci :
ami = [ "Jean-Paul", "Goret", "Rue de l'église, 26", "Houtsiplou", "Liège" ]
Évidemment, ceci fonctionnera. Mais nous allons devoir nous souvenir que ami[0] pointe vers le prénom, ami[1] vers le nom de famille, et ainsi de suite. Ceci peut se révéler fort compliqué par la suite.
C'est exactement le type de problème qu'un hachage peut résoudre. Voici la définition d'un hachage en Ruby :
ami = {
"prénom" =>"Jean-Paul",
"nom de famille" =>"Goret",
"adresse" =>"Rue de l'église, 26",
"ville" =>"Houtsiplou",
"province" =>"Liège"
}
Remarquez bien qu'il faut utiliser des accolades pour créer un hachage. Les tableaux, quant à eux, se définissent par l'intermédiaire de crochets.
Comme les tableaux, il est tout à fait possible de rajouter par la suite des champs à un hachage :
ami["pays"] = "Belgique"
III-D-1. Quelques définitions : clef et valeur▲
Comme les hachages n'utilisent pas que des indices numériques, on utilise le mot clef (en anglais : «key») pour les identifier. Un objet représenté à travers une clef est appelé valeur (en anglais : «value»).
Donc, dans l'exemple précédent, «prénom», «nom de famille» et «pays» sont des clefs. Leurs valeurs respectives sont «Jean-Paul», «Goret» et «Belgique».
III-D-2. Hachages et itérateurs▲
Évidemment, les hachages possèdent des itérateurs. En voici quelques-uns
III-D-2-a. Hash#each▲
À l'instar des tableaux, les hachages ont également une méthode each. Cependant, elle nous donne à la fois la clef et la valeur de l'entrée. En voici un exemple :
amis.each do |clef, valeur|
puts clef + " =>" + valeur
end
Et voici son résultat à l'écran :
ville =>Houtsiplou
nom de famille =>Goret
pays =>Belgique
adresse =>Rue de l'église, 26
province =>Liège
prénom =>Jean-Paul
Ceci illustre un des plus gros défauts des hachages. Les données contenues dans un hachage ne sont pas classées dans un ordre particulier. Comment Ruby pourrait-il savoir que « prénom » doit venir avant « adresse » ?
Le meilleur moyen de résoudre ce type de problème est de créer sa propre classe. Ruby est excellent pour ça. Mais avant d'étudier cette technique, il y a encore quelques notions que nous devons voir. Le prochain chapitre vous expliquera comment créer vos propres classes et méthodes.
Hash#each_pair est un synonyme de Hash#each
III-D-2-b. Hash#each_key▲
Le nom de cette méthode nous dit tout. Elle permet de parcourir toutes les clefs d'un hachage, un peu de la même façon que Hash#each :
>>ami.each_key do |clef|
?> puts clef
>>end
ville
nom de famille
pays
adresse
province
prénom
III-D-2-c. Hash#each_value▲
Cette méthode parcourt toutes les valeurs d'un hachage :
>>ami.each_value do |valeur|
?> puts valeur
>>end
Houtsiplou
Goret
Belgique
Rue de l'église, 26
Liège
Jean-Paul
III-E. Exemple pratique : un carnet d'adresses▲
Dans cette section, nous allons construire un petit carnet d'adresses, contenant des informations sur trois amis : Nicolas, François et Marina.
La structure d'un carnet d'adresses est relativement complexe. Elle doit contenir plusieurs personnes, chacun ayant un nom, une adresse, et ainsi de suite.
Pour implémenter ce carnet d'adresses, notre stratégie sera de découper le problème en plusieurs petits problèmes.
III-E-1. Première étape : analyse▲
Il nous faut commencer par déterminer le type d'informations que notre carnet d'adresses devrait référencer :
- Le carnet d'adresses doit contenir une liste de personnes. Nous devrions être capables de trier ces personnes par ordre alphabétique ;
- Chaque personne possède évidemment un prénom, un nom de famille, un numéro de téléphone ainsi qu'une adresse ;
- Chaque adresse est définie par le nom d'une rue, d'un code postal ainsi que d'une ville et le pays dans lequel elle se situe.
Nous allons commencer par définir une structure représentant une adresse, et nous finirons par assembler le carnet tout entier.
III-E-2. Deuxième étape : adresses▲
Nous avons ici deux possibilités pour implémenter la structure d'une adresse :
- Tableau : nous pourrions ajouter successivement l'adresse, le code postal, la ville et le pays dans un tableau. Ça fonctionnerait ;
- Hachage : évidemment, c'est plus facile de se souvenir d'une expression comme adresse[« ville »] plutôt que adresse[2].
Nous allons évidemment choisir d'utiliser un hachage. Voici la représentation de nos adresses :
# Adresse de Nicolas
adresse_de_nicolas = {
"rue" =>"Rue du port, 32",
"code postal" =>"56000",
"ville" =>"Vannes",
"pays" =>"France"
}
# Adresse de François
adresse_de_francois = {
"rue" =>"Avenue de la tranchée, 14",
"code postal" =>"1000",
"ville" =>"Bruxelles",
"pays" =>"Belgique"
}
# Adresse de Marina
adresse_de_marina = {
"rue" =>"Strada di l'amore, 61",
"code postal" =>"50100",
"ville" =>"Firenze",
"pays" =>"Italia"
}
III-E-3. Troisième étape : personnes▲
Chaque personne est identifiée par un prénom, un nom de famille, un numéro de téléphone ainsi qu'une adresse. Comme pour la structure précédente, un hachage convient parfaitement. Voici comment nous pouvons représenter nos amis avec Ruby :
# Nicolas
nicolas = {
"prénom" =>"Nicolas",
"nom de famille" =>"Rocher",
"téléphone" =>"(+33) 02 93 45 49 19",
"adresse" =>adresse_de_nicolas
}
# François
francois = {
"prénom" =>"François",
"nom de famille" =>"Willemart",
"téléphone" =>"(+32) 02 679 24 81",
"adresse" =>adresse_de_francois
}
# Marina
marina = {
"prénom" =>"Marina",
"nom de famille" =>"Nantini",
"téléphone" =>"(+39) 055 681 32 11",
"adresse" =>adresse_de_marina
}
III-E-4. Quatrième étape : carnet d'adresses▲
Maintenant que nous avons défini toutes les autres structures, il ne nous reste plus qu'à créer le carnet d'adresses. Comme nous désirons pouvoir par la suite trier les éléments, nous devons donc utiliser un tableau pour réunir nos trois amis :
carnet = [ nicolas, francois, marina ]
Et voilà ! Nous venons d'implémenter complètement la structure de notre carnet d'adresses. Dans la section suivante, nous allons apprendre comment trier les éléments du carnet, ainsi que d'autres petites choses amusantes.
Sauvegardez les structures dans un fichier. Nous allons les réutiliser dans les sections qui vont suivre.
III-E-5. Quelques exercices▲
- Ajoutez un de vos amis dans le carnet d'adresses.
- Modifiez la structure du carnet d'adresses afin qu'il puisse contenir des adresses e-mail.
III-F. Afficher le contenu du carnet▲
Nous allons apprendre dans cette section comment afficher à l'écran le contenu de notre carnet d'adresses.
III-F-1. Afficher des structures de données complexes▲
Nous pouvons toujours taper puts carnet, mais le résultat n'est pas très lisible pour un être humain (essayez dans IRB par vous-même). Nous voudrions mieux définir notre propre moyen pour afficher son contenu.
Comme carnet est un tableau, nous pouvons utiliser l'itérateur Array#each. Commençons par simplement afficher le prénom de nos amis :
carnet.each do |personne|
puts personne["prénom"]
end
Ce qui affichera :
Nicolas
François
Marina
III-F-2. Noms complets▲
La prochaine étape est d'afficher les noms complets :
carnet.each do |personne|
prenom = personne["prénom"]
nom = personne["nom de famille"]
puts prenom + " " + nom
end
Nicolas Rocher
François Willemart
Marina Nantini
III-F-3. Numéros de téléphone▲
Rajouter le numéro de téléphone n'est pas plus difficile :
carnet.each do |personne|
prenom = personne["prénom"]
nom = personne["nom de famille"]
tel = personne["téléphone"]
puts prenom + " " + nom
puts " " + tel
end
Le résultat :
Nicolas Rocher
(+33) 02 93 45 49 19
François Willemart
(+32) 02 679 24 81
Marina Nantini
(+39) 055 681 32 11
III-F-4. Adresses▲
Finalement, il ne nous reste plus qu'à afficher les adresses. Voici le code qui devrait effectuer cette requête, en prenant soin d'espacer chaque personne par une nouvelle ligne :
carnet.each do |personne|
# Nom et téléphone
prenom = personne["prénom"]
nom = personne["nom de famille"]
tel = personne["téléphone"]
puts prenom + " " + nom
puts " " + tel
# Adresse
rue = personne["adresse"]["rue"]
cp = personne["adresse"]["code postal"]
ville = personne["adresse"]["ville"]
pays = personne["adresse"]["pays"]
puts " " + rue
puts " " + cp + ", " + ville
puts " " + pays
# Une ligne vide pour séparer les entrées
puts ""
end
Ce qui produira :
Nicolas Rocher
(+33) 02 93 45 49 19
Rue du port, 32
56000, Vannes
France
François Willemart
(+32) 02 679 24 81
Avenue de la tranchée, 14
1000, Bruxelles
Belgique
Marina Nantini
(+39) 055 681 32 11
Strada di l'amore, 61
50100, Firenze
Italia
Et voilà le travail !
III-G. Trier les éléments du carnet▲
Souvenez-vous, nous avons choisi d'utiliser un tableau pour contenir les différentes personnes de notre carnet d'adresses, parce que nous avions décidé au début qu'il devrait être possible de trier ces personnes, selon des critères quelconques.
Il est maintenant temps de trier les éléments de notre carnet.
III-G-1. Retour sur Array#sort▲
Nous avons déjà utilisé Array#sort pour trier des tableaux contenant des nombres entiers, et des chaines de caractères. Mais comment pouvons-nous trier des tableaux contenant des structures de données plus complexes (comme nos hachages) ?
Lorsque le comportement par défaut n'est pas ce que vous désirez, Array#sort vous permet de spécifier comment trier le tableau. Commençons avec un exemple beaucoup plus facile que notre carnet d'adresses actuel :
amis = [
[ "Nicolas", "Rocher" ],
[ "François", "Willemart" ],
[ "Marina", "Nantini" ]
]
Nous savons que par défaut la méthode Array#sort appelée sur l'objet amis effectuera le tri en fonction du premier élément de chaque sous-tableau - dans ce cas, les prénoms. Et si nous voulions trier ces éléments par leur nom de famille ?
À l'instar des itérateurs, la méthode Array#sort peut accepter un bloc de code. Comme ceci :
amis.sort do |a, b|
# ...
end
La méthode Array#sort attend trois comportements de ce bloc de code :
- Retourner la valeur -1 si la variable a est jugée plus petite que la variable b ;
- Retourner la valeur 0 si la variable a est jugée égale à la variable b ;
- Retourner la valeur 1 si la variable a est jugée plus grande que la variable b.
Grâce a cette information, la méthode Array#sort peut savoir exactement comment il faut trier le tableau.
III-G-2. Retourner des valeurs▲
Comment faire pour retourner une valeur? La valeur de retour est tout simplement la valeur de la dernière expression interprétée. Jetons un petit coup d'œil dans IRB :
Les lignes =>sont les valeurs de retour des expressions.
III-G-3. L'opérateur <=>▲
Trier étant une opération fort courante, il existe donc un opérateur spécialement dédié à ça. L'opérateur <=>retourne pour nous les fameuses valeurs -1, 0 et 1. Essayez ceci dans IRB :
III-G-4. Trier sur le nom de famille▲
Retournons à notre petit tableau :
amis = [
[ "Nicolas", "Rocher" ],
[ "François", "Willemart" ],
[ "Marina", "Nantini" ]
]
Et à notre instruction de triage :
amis.sort do |a, b|
# ...
end
a et b sont des éléments du tableau amis. Le comportement par défaut de Array#sort est équivalent à :
amis.sort do |a, b|
a[0] <=>b[0] # Tri sur le premier élément
end
Mais comme nous voulons trier sur le second élément (c'est-à-dire le nom de famille), nous devons écrire :
amis = amis.sort do |a, b|
a[1] <=>b[1] # Tri sur le second élément
end
# Maintenant, les éléments du tableau 'amis' sont triés sur le nom de famille.
III-G-5. Trier le carnet d'adresses▲
Maintenant que nous connaissons Array#sort un peu mieux, nous sommes prêts à personnaliser le tri du carnet d'adresses. Nous avons :
carnet.sort do |personne_a, personne_b|
# ...
end
personne_a et personne_b sont tous deux des objets provenant du même hachage. Nous pouvons donc, par exemple, les trier par ordre alphabétique sur leur prénom, en écrivant tout simplement ceci :
carnet.sort do |personne_a, personne_b|
personne_a["prénom"] <=>personne_b["prénom"]
end
Essayez, ça fonctionne !
III-G-6. Quelques exercices▲
Écrivez le code Ruby permettant de trier chaque élément du tableau carnet en fonction du nom complet (c'est-à-dire, le prénom et le nom de famille).
III-H. Écrire de bons programmes▲
III-H-1. Tableau ou hachage ?▲
Quand faut-il utiliser un tableau ? Quand faut-il utiliser un hachage ?
La structure du carnet d'adresses est un bon exemple pour répondre à ces deux questions.
- Si vos données doivent être triées, il vous faut un tableau.
- Si vos données représentent une collection de données différentes, sans aucune corrélation entre elles (par exemple, un prénom, un nom et un numéro de téléphone), il vous faut un hachage.
- Par contre, si les éléments appartiennent à la même catégorie de données (par exemple, une liste de noms), il vous faut un tableau.
Dans l'exemple du carnet d'adresses, les structures pour les adresses et les personnes contiennent des données différentes. Donc, nous les avons représentés par des hachages.
Mais le carnet lui-même contient des données du même type (des personnes), et nous voulions également être capables de trier ses éléments. Donc, représenter le carnet par un tableau était le meilleur choix.
III-H-2. Noms de variables▲
Comme vous êtes maintenant capable d'écrire des structures de données complexes, il devient plus important que vous choisissiez de bons noms pour vos variables.
III-H-2-a. Tableaux▲
Un tableau devrait toujours représenter une collection d'éléments du même type. Comme une groupe de voitures, une liste de noms, etc.. Vous pouvez représenter ceci en utiliser en mettant au pluriel nom de votre tableau. Par exemple :
- si chaque élément du tableau est une voiture, le tableau devrait s'appeler voitures ;
- si chaque élément du tableau est le nom d'une personne, le tableau devrait s'appeler noms.
De cette façon, le nom de la variable vous rappelle qu'il s'agit d'un tableau, et en même temps, elle se lit comme du français.
III-H-2-b. Hachages▲
Lorsque vous travaillerez avec des hachages, il est important que vous choisissiez de bons noms pour vos clefs de hachage. Un bon nom doit être à la fois clair, descriptif et facile à se souvenir.
- Bien : « deuxième prénom »
- Pas bien : « 2eprenom »
N'oubliez pas que Ruby accepte des espaces et des accents comme noms pour vos clefs de hachage. Vous n'avez donc aucune excuse d'utiliser « 2eprenom » pour une clef.
III-H-3. Commentaires▲
N'oubliez surtout pas de rajouter des commentaires clairs dans vos programmes. Souvenez-vous, on écrit le code une fois, et on le lit une infinité de fois.
III-H-4. Sous-structures▲
En général, il est recommandé d'éviter de déclarer des sous-structures. Néanmoins, si vous utilisez une indentation propre et des noms significatifs pour vos variables, ça peut être bon.
Jetez un coup d'œil à ces exemples :
III-H-4-a. Bien▲
# Adresse de Nicolas
adresse_de_nicolas = {
"rue" =>"Rue du port, 32",
"code postal" =>"56000",
"ville" =>"Vannes",
"pays" =>"France"
}
# Nicolas
nicolas = {
"prénom" =>"Nicolas",
"nom de famille" =>"Rocher",
"téléphone" =>"(+33) 02 93 45 49 19",
"adresse" =>adresse_de_nicolas
}
III-H-4-b. Peut mieux faire▲
# Nicolas
nicolas = {
"prénom" =>"Nicolas",
"nom de famille" =>"Rocher",
"téléphone" =>"(+33) 02 93 45 49 19",
"adresse" =>{
"rue" =>"Rue du port, 32",
"code postal" =>"56000",
"ville" =>"Vannes",
"pays" =>"France"
}
}
III-H-4-c. Pas bien▲
# Nicolas
nicolas = {
"prénom" =>"Nicolas",
"nom de famille" =>"Rocher",
"téléphone" =>"(+33) 02 93 45 49 19",
"adresse" =>{
"rue" =>"Rue du port, 32",
"code postal" =>"56000",
"ville" =>"Vannes",
"pays" =>"France"
}}