Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Un exemple de programme utilisant les structs

Pour comprendre quand nous pourrions vouloir utiliser des structs, écrivons un programme qui calcule l’aire d’un rectangle. Nous commencerons par utiliser des variables simples, puis nous refactoriserons le programme jusqu’à utiliser des structs à la place.

Créons un nouveau projet binaire avec Cargo appelé rectangles qui prendra la largeur et la hauteur d’un rectangle spécifiées en pixels et calculera l’aire du rectangle. Le listing 5-8 montre un court programme avec une manière de faire exactement cela dans le fichier src/main.rs de notre projet.

Filename: src/main.rs
fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}
Listing 5-8: Calculating the area of a rectangle specified by separate width and height variables

Maintenant, exécutez ce programme avec cargo run : console {{#include ../listings/ch05-using-structs-to-structure-related-data/listing-05-08/output.txt}}

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.

Ce code réussit à déterminer l’aire du rectangle en appelant la fonction area avec chaque dimension, mais nous pouvons faire davantage pour rendre ce code clair et lisible.

Le problème avec ce code est évident dans la signature de area : rust,ignore {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-08/src/main.rs:here}}

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

La fonction area est censée calculer l’aire d’un seul rectangle, mais la fonction que nous avons écrite a deux paramètres, et rien dans notre programme n’indique clairement que ces paramètres sont liés. Il serait plus lisible et plus facile à gérer de regrouper la largeur et la hauteur. Nous avons déjà abordé une façon de le faire dans la section [« Le type tuple »][the-tuple-type] du chapitre 3 : en utilisant des tuples.

Refactorisation avec des tuples

Le listing 5-9 montre une autre version de notre programme qui utilise des tuples.

Filename: src/main.rs
fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}
Listing 5-9: Specifying the width and height of the rectangle with a tuple

D’un côté, ce programme est meilleur. Les tuples nous permettent d’ajouter un peu de structure, et nous ne passons maintenant qu’un seul argument. Mais d’un autre côté, cette version est moins claire : les tuples ne nomment pas leurs éléments, donc nous devons accéder aux parties du tuple par index, ce qui rend notre calcul moins évident.

Confondre la largeur et la hauteur n’aurait pas d’importance pour le calcul de l’aire, mais si nous voulions dessiner le rectangle à l’écran, cela compterait ! Nous devrions garder en tête que width est l’index 0 du tuple et height est l’index 1. Ce serait encore plus difficile pour quelqu’un d’autre de comprendre et de retenir s’il devait utiliser notre code. Comme nous n’avons pas exprimé la signification de nos données dans notre code, il est maintenant plus facile d’introduire des erreurs.

Refactorisation avec des structs

Nous utilisons les structs pour ajouter du sens en étiquetant les données. Nous pouvons transformer le tuple que nous utilisons en une struct avec un nom pour l’ensemble ainsi que des noms pour les parties, comme le montre le listing 5-10.

Filename: src/main.rs
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}
Listing 5-10: Defining a Rectangle struct

Ici, nous avons défini une struct et l’avons nommée Rectangle. À l’intérieur des accolades, nous avons défini les champs width et height, qui sont tous les deux de type u32. Ensuite, dans main, nous avons créé une instance particulière de Rectangle avec une largeur de 30 et une hauteur de 50.

Notre fonction area est maintenant définie avec un seul paramètre, que nous avons nommé rectangle, dont le type est un emprunt immutable d’une instance de la struct Rectangle. Comme mentionné au chapitre 4, nous voulons emprunter la struct plutôt que d’en prendre possession. De cette façon, main conserve la possession et peut continuer à utiliser rect1, c’est pourquoi nous utilisons le & dans la signature de la fonction et à l’endroit où nous appelons la fonction.

La fonction area accède aux champs width et height de l’instance de Rectangle (notez que l’accès aux champs d’une instance de struct empruntée ne déplace pas les valeurs des champs, c’est pourquoi vous voyez souvent des emprunts de structs). Notre signature de fonction pour area dit maintenant exactement ce que nous voulons dire : calculer l’aire d’un Rectangle en utilisant ses champs width et height. Cela exprime 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 d’index de tuple 0 et 1. C’est un gain de clarté.

Ajouter des fonctionnalités avec les traits dérivés

Il serait utile de pouvoir afficher une instance de Rectangle pendant que nous déboguons notre programme et de voir les valeurs de tous ses champs. Le listing 5-11 essaie d’utiliser la [macro println!][println] comme nous l’avons fait dans les chapitres précédents. Cependant, cela ne fonctionnera pas.

Filename: src/main.rs
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1}");
}
Listing 5-11: Attempting to print a Rectangle instance

Lorsque nous compilons ce code, nous obtenons une erreur avec ce message principal : text {{#include ../listings/ch05-using-structs-to-structure-related-data/listing-05-11/output.txt:3}}

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

La macro println! peut effectuer de nombreux types de formatage, et par défaut, les accolades indiquent à println! d’utiliser le formatage connu sous le nom de Display : une sortie destinée à la consommation directe par l’utilisateur final. Les types primitifs que nous avons vus jusqu’ici implémentent Display par défaut car il n’y a qu’une seule façon de montrer un 1 ou tout autre type primitif à un utilisateur. Mais avec les structs, la façon dont println! devrait formater la sortie est moins claire car il y a plus de possibilités d’affichage : voulez-vous des virgules ou non ? Voulez-vous afficher les accolades ? Tous les champs doivent-ils être affichés ? En raison de cette ambiguïté, Rust n’essaie pas de deviner ce que nous voulons, et les structs n’ont pas d’implémentation fournie de Display à utiliser avec println! et le placeholder {}.

Si nous continuons à lire les erreurs, nous trouverons cette note utile : text {{#include ../listings/ch05-using-structs-to-structure-related-data/listing-05-11/output.txt:9:10}}

   |                        |`Rectangle` cannot be formatted with the default formatter
   |                        required by this formatting parameter

Essayons ! L’appel à la macro println! ressemblera maintenant à println!("rect1 is {rect1:?}");. Mettre le spécificateur :? à l’intérieur des accolades indique à println! que nous voulons utiliser un format de sortie appelé Debug. Le trait Debug nous permet d’afficher notre struct d’une manière utile pour les développeurs afin que nous puissions voir sa valeur pendant que nous déboguons notre code.

Compilez le code avec cette modification. Zut ! Nous obtenons toujours une erreur : text {{#include ../listings/ch05-using-structs-to-structure-related-data/output-only-01-debug/output.txt:3}}

error[E0277]: `Rectangle` doesn't implement `Debug`

Mais encore une fois, le compilateur nous donne une note utile : text {{#include ../listings/ch05-using-structs-to-structure-related-data/output-only-01-debug/output.txt:9:10}}

   |                        required by this formatting parameter
   |

Rust inclut bien la fonctionnalité pour afficher des informations de débogage, mais nous devons explicitement l’activer pour rendre cette fonctionnalité disponible pour notre struct. Pour ce faire, nous ajoutons l’attribut externe #[derive(Debug)] juste avant la définition de la struct, comme le montre le listing 5-12.

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1:?}");
}
Listing 5-12: Adding the attribute to derive the Debug trait and printing the Rectangle instance using debug formatting

Maintenant, quand nous exécutons le programme, nous n’obtiendrons aucune erreur, et nous verrons la sortie suivante : console {{#include ../listings/ch05-using-structs-to-structure-related-data/listing-05-12/output.txt}}

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }

Bien ! Ce n’est pas la plus jolie sortie, mais elle montre les valeurs de tous les champs pour cette instance, ce qui aiderait certainement lors du débogage. Quand nous avons des structs plus grandes, il est utile d’avoir une sortie un peu plus facile à lire ; dans ces cas-là, nous pouvons utiliser {:#?} au lieu de {:?} dans la chaîne de println!. Dans cet exemple, utiliser le style {:#?} produira la sortie suivante : console {{#include ../listings/ch05-using-structs-to-structure-related-data/output-only-02-pretty-debug/output.txt}}

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/rectangles`
rect1 is Rectangle {
    width: 30,
    height: 50,
}

Une autre façon d’afficher une valeur en utilisant le format Debug est d’utiliser la macro dbg!, qui prend possession d’une expression (contrairement à println!, qui prend une référence), affiche le fichier et le numéro de ligne où l’appel à la macro dbg! se produit dans votre code ainsi que la valeur résultante de cette expression, et retourné la possession de la valeur.

Note : L’appel à la macro dbg! affiche sur le flux d’erreur standard de la console (stderr), contrairement à println!, qui affiche sur le flux de sortie standard de la console (stdout). Nous parlerons davantage de stderr et stdout dans la section [« Rediriger les erreurs vers la sortie d’erreur standard » du chapitre 12][err].

Voici un exemple où nous nous intéressons à la valeur assignée au champ width, ainsi qu’à la valeur de la struct entière dans rect1 : rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-05-dbg-macro/src/main.rs}}

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

Nous pouvons placer dbg! autour de l’expression 30 * scale et, comme dbg! retourné la possession de la valeur de l’expression, le champ width recevra la même valeur que si nous n’avions pas l’appel à dbg! à cet endroit. Nous ne voulons pas que dbg! prenne possession de rect1, donc nous utilisons une référence vers rect1 dans l’appel suivant. Voici à quoi ressemble la sortie de cet exemple : console {{#include ../listings/ch05-using-structs-to-structure-related-data/no-listing-05-dbg-macro/output.txt}}

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

Nous pouvons voir que la première partie de la sortie provient de src/main.rs ligne 10 où nous déboguons l’expression 30 * scale, et sa valeur résultante est 60 (le formatage Debug implémenté pour les entiers consiste à afficher uniquement leur valeur). L’appel à dbg! à la ligne 14 de src/main.rs affiche la valeur de &rect1, qui est la struct Rectangle. Cette sortie utilise le joli formatage Debug du type Rectangle. La macro dbg! peut être vraiment utile quand vous essayez de comprendre ce que fait votre code !

En plus du trait Debug, Rust fournit un certain nombre de traits à utiliser avec l’attribut derive qui peuvent ajouter des comportements utiles à nos types personnalisés. Ces traits et leurs comportements sont listés dans l’[annexe C][app-c]. Nous verrons comment implémenter ces traits avec un comportement personnalisé ainsi que comment créer vos propres traits au chapitre 10. Il existe également de nombreux attributs autres que derive ; pour plus d’informations, consultez [la section « Attributes » de la référence Rust][attributes].

Notre fonction area est très spécifique : elle ne calcule que l’aire de rectangles. Il serait utile de lier ce comportement plus étroitement à notre struct Rectangle car il ne fonctionnera avec aucun autre type. Voyons comment nous pouvons continuer à refactoriser ce code en transformant la fonction area en une méthode area définie sur notre type Rectangle.