Un exemple de programme qui utilise des structures
Pour comprendre dans quels cas nous voudrions utiliser des structures, écrivons un programme qui calcule l'aire d'un rectangle. Nous commencerons en utilisant de simples variables, puis on remaniera le code jusqu'à utiliser des structures à la place.
Créons un nouveau projet binaire avec Cargo nommé rectangles qui prendra la largeur et la hauteur en pixels d'un rectangle et qui calculera l'aire de ce rectangle. L'encart 5-8 montre un petit programme qui effectue cette tâche d'une certaine manière dans le src/main.rs de notre projet.
Fichier: src/main.rs
fn main() { let largeur1 = 30; let hauteur1 = 50; println!( "L'aire du rectangle est de {} pixels carrés.", aire(largeur1, hauteur1) ); } fn aire(largeur: u32, hauteur: u32) -> u32 { largeur * hauteur }
Maintenant, lancez ce programme avec cargo run
:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
L'aire du rectangle est de 1500 pixels carrés.
Ce code arrive à déterminer l'aire du rectangle en appelant la fonction aire
avec chaque dimension, mais on peut faire mieux pour clarifier ce code et le
rendre plus lisible.
Le problème de ce code se voit dans la signature de aire
:
fn main() {
let largeur1 = 30;
let hauteur1 = 50;
println!(
"L'aire du rectangle est de {} pixels carrés.",
aire(largeur1, hauteur1)
);
}
fn aire(largeur: u32, hauteur: u32) -> u32 {
largeur * hauteur
}
La fonction aire
est censée calculer l'aire d'un rectangle, mais la fonction
que nous avons écrite a deux paramètres, et il n'est pas précisé nulle part
dans notre programme à quoi sont liés les paramètres. Il serait plus lisible et
plus gérable de regrouper ensemble la largeur et la hauteur. Nous avons déjà vu
dans la section “Le type tuple” du chapitre 3
une façon qui nous permettrait de le faire : en utilisant des tuples.
Remanier le code avec des tuples
L'encart 5-9 nous montre une autre version de notre programme qui utilise des tuples.
Fichier : src/main.rs
fn main() { let rect1 = (30, 50); println!( "L'aire du rectangle est de {} pixels carrés.", aire(rect1) ); } fn aire(dimensions: (u32, u32)) -> u32 { dimensions.0 * dimensions.1 }
D'une certaine façon, ce programme est meilleur. Les tuples nous permettent de structurer un peu plus et nous ne passons plus qu'un argument. Mais d'une autre façon, cette version est moins claire : les tuples ne donnent pas de noms à leurs éléments, donc il faut accéder aux éléments du tuple via leur indice, ce qui rends plus compliqué notre calcul.
Le mélange de la largeur et la hauteur n'est pas important pour calculer l'aire,
mais si on voulait afficher le rectangle à l'écran, cela serait problématique !
Il nous faut garder à l'esprit que la largeur
est l'élément à l'indice 0
du
tuple et que la hauteur
est l'élément à l'indice 1
. Cela complexifie le
travail de quelqu'un d'autre de le comprendre et s'en souvenir pour qu'il
puisse l'utiliser. Comme on n'a pas exprimé la signification de nos données
dans notre code, il est plus facile de faire des erreurs.
Remanier avec des structures : donner plus de sens
On utilise des structures pour rendre les données plus expressives en leur donnant des noms. On peut transformer le tuple que nous avons utilisé en une structure nommée dont ses éléments sont aussi nommés, comme le montre l'encart 5-10.
Fichier : src/main.rs
struct Rectangle { largeur: u32, hauteur: u32, } fn main() { let rect1 = Rectangle { largeur: 30, hauteur: 50 }; println!( "L'aire du rectangle est de {} pixels carrés.", aire(&rect1) ); } fn aire(rectangle: &Rectangle) -> u32 { rectangle.largeur * rectangle.hauteur }
Ici, on a défini une structure et on l'a appelée Rectangle
. Entre les
accolades, on a défini les champs largeur
et hauteur
, tous deux du type
u32
. Puis dans main
, on crée une instance de Rectangle
de largeur 30 et de
hauteur 50.
Notre fonction aire
est désormais définie avec un unique paramètre, nommé
rectangle
, et dont le type est une référence immuable vers une instance de la
structure Rectangle
. Comme mentionné au chapitre 4, on préfère emprunter la
structure au lieu d'en prendre possession. Ainsi, elle reste en possession de
main
qui peut continuer à utiliser rect1
; c'est pourquoi on utilise le &
dans la signature de la fonction ainsi que dans l'appel de fonction.
La fonction aire
accède aux champs largeur
et hauteur
de l'instance de
Rectangle
. Notre signature de fonction pour aire
est enfin explicite :
calculer l'aire d'un Rectangle
en utilisant ses champs largeur
et hauteur
.
Cela explique que la largeur et la hauteur sont liées entre elles, et cela donne
des noms descriptifs aux valeurs plutôt que d'utiliser les valeurs du tuple avec
les indices 0
et 1
. On gagne en clarté.
Ajouter des fonctionnalités utiles avec les traits dérivés
Cela serait pratique de pouvoir afficher une instance de Rectangle
pendant
qu'on débogue notre programme et de voir la valeur de chacun de ses champs.
L'encart 5-11 essaye de le faire en utilisant la macro
println!
comme on l'a fait dans les chapitres
précédents. Cependant, cela ne fonctionne pas.
Fichier : src/main.rs
struct Rectangle {
largeur: u32,
hauteur: u32,
}
fn main() {
let rect1 = Rectangle {
largeur: 30,
hauteur: 50
};
println!("rect1 est {}", rect1);
}
Lorsqu'on compile ce code, on obtient ce message d'erreur qui nous informe que
Rectangle
n'implémente pas le trait std::fmt::Display
:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
La macro println!
peut faire toutes sortes de formatages textuels, et par
défaut, les accolades demandent à println!
d'utiliser le formatage appelé
Display
, pour convertir en texte destiné à être vu par l'utilisateur final.
Les types primitifs qu'on a vus jusqu'ici implémentent Display
par défaut
puisqu'il n'existe qu'une seule façon d'afficher un 1
ou tout autre type
primitif à l'utilisateur. Mais pour les structures, la façon dont println!
devrait formater son résultat est moins claire car il y a plus de possibilités
d'affichage : Voulez-vous des virgules ? Voulez-vous afficher les accolades ?
Est-ce que tous les champs devraient être affichés ? À cause de ces ambiguïtés,
Rust n'essaye pas de deviner ce qu'on veut, et les structures n'implémentent pas
Display
par défaut pour l'utiliser avec println!
et les espaces réservés
{}
.
Si nous continuons de lire les erreurs, nous trouvons cette remarque utile :
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
Le compilateur nous informe que dans notre chaîne de formatage, on est peut-être
en mesure d'utiliser {:?}
(ou {:#?}
pour un affichage plus élégant).
Essayons cela ! L'appel de la macro println!
ressemble maintenant à
println!("rect1 est {:?}", rect1);
. Insérer le sélecteur :?
entre les
accolades permet d'indiquer à println!
que nous voulons utiliser le formatage
appelé Debug
. Le trait Debug
nous permet d'afficher notre structure d'une
manière utile aux développeurs pour qu'on puisse voir sa valeur pendant qu'on
débogue le code.
Compilez le code avec ce changement. Zut ! On a encore une erreur, nous
informant cette fois-ci que Rectangle
n'implémente pas std::fmt::Debug
:
error[E0277]: `Rectangle` doesn't implement `Debug`
Mais une nouvelle fois, le compilateur nous fait une remarque utile :
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
Il nous conseille d'ajouter #[derive(Debug)]
ou d'implémenter manuellement
std::fmt::Debug
.
Rust inclut bel et bien une fonctionnalité pour afficher des informations de
débogage, mais nous devons l'activer explicitement pour la rendre disponible sur
notre structure. Pour ce faire, on ajoute l'attribut externe #[derive(Debug)]
juste avant la définition de la structure, comme le montre l'encart 5-12.
Fichier : src/main.rs
#[derive(Debug)] struct Rectangle { largeur: u32, hauteur: u32, } fn main() { let rect1 = Rectangle { largeur: 30, hauteur: 50 }; println!("rect1 est {:?}", rect1); }
Maintenant, quand on exécute le programme, nous n'avons plus d'erreurs et ce texte s'affiche à l'écran :
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 est Rectangle { largeur: 30, hauteur: 50 }
Super ! Ce n'est pas le plus beau des affichages, mais cela montre les
valeurs de tous les champs de cette instance, ce qui serait assurément utile
lors du débogage. Quand on a des structures plus grandes, il serait bien d'avoir
un affichage un peu plus lisible ; dans ces cas-là, on pourra utiliser {:#?}
au lieu de {:?}
dans la chaîne de formatage. Dans cette exemple,
l'utilisation du style {:#?}
va afficher ceci :
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 est Rectangle {
largeur: 30,
hauteur: 50,
}
Une autre façon d'afficher une valeur en utilisant le format Debug
est
d'utiliser la macro dbg!
, qui prend possession de
l'expression, affiche le nom du fichier et la ligne de votre code où se trouve
cet appel à la macro dbg!
ainsi que le résultat de cette expression, puis
rend la possession de cette valeur.
Remarque : l'appel à la macro
dbg!
écrit dans le flux d'erreur standard de la console (stderr
), contrairement àprintln!
qui écrit dans le flux de sortie standard de la console (stdout
). Nous reparlerons destderr
et destdout
dans une section du chapitre 12.
Voici un exemple dans lequel nous nous intéressons à la valeur assignée au
champ largeur
, ainsi que la valeur de toute la structure rect1
:
#[derive(Debug)] struct Rectangle { largeur: u32, hauteur: u32, } fn main() { let echelle = 2; let rect1 = Rectangle { largeur: dbg!(30 * echelle), hauteur: 50, }; dbg!(&rect1); }
Nous pouvons placer le dbg!
autour de l'expression 30 * echelle
et, comme
dbg!
retourne la possession de la valeur issue de l'expression, le champ
largeur
va avoir la même valeur que si nous n'avions pas appelé dbg!
ici.
Nous ne voulons pas que dbg!
prenne possession de rect1
, donc nous donnons
une référence à rect1
lors de son prochain appel. Voici à quoi ressemble la
sortie de cet exemple :
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10] 30 * echelle = 60
[src/main.rs:14] &rect1 = Rectangle {
largeur: 60,
hauteur: 50,
}
Nous pouvons constater que la première sortie provient de la ligne 10
de src/main.rs, où nous déboguons l'expression 30 * echelle
, et son résultat
est 60 (le formattage de Debug
pour les entiers est d'afficher uniquement sa
valeur). L'appel à dbg!
à la ligne 14 de src/main.rs affiche la valeur de
&rect1
, qui est une structure Rectangle
. La macro dbg!
peut être très
utile lorsque vous essayez de comprendre ce que fait votre code !
En plus du trait Debug
, Rust nous offre d'autres traits pour que nous
puissions les utiliser avec l'attribut derive
pour ajouter des comportements
utiles à nos propres types. Ces traits et leurs comportements sont listés à
l'annexe C. Nous expliquerons comment implémenter ces
traits avec des comportements personnalisés et comment créer vos propres traits
au chapitre 10. Il existe aussi de nombreux attributs autres que derive
; pour
en savoir plus, consultez la section “Attributs” de la référence de
Rust.
Notre fonction aire
est très spécifique : elle ne fait que calculer l'aire
d'un rectangle. Il serait utile de lier un peu plus ce comportement à notre
structure Rectangle
, puisque cela ne fonctionnera pas avec un autre type.
Voyons comment on peut continuer de remanier ce code en transformant la fonction
aire
en méthode aire
définie sur notre type Rectangle
.