Un exemple de programme utilisant des structs

RustRustBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Bienvenue dans Un exemple de programme utilisant des structs. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous allons écrire un programme utilisant des structs pour calculer l'aire d'un rectangle, en refactorisant le code initial qui utilisait des variables séparées pour la largeur et la hauteur.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") subgraph Lab Skills rust/variable_declarations -.-> lab-100396{{"Un exemple de programme utilisant des structs"}} rust/integer_types -.-> lab-100396{{"Un exemple de programme utilisant des structs"}} rust/function_syntax -.-> lab-100396{{"Un exemple de programme utilisant des structs"}} rust/expressions_statements -.-> lab-100396{{"Un exemple de programme utilisant des structs"}} end

Un exemple de programme utilisant des structs

Pour comprendre dans quels cas nous pourrions vouloir utiliser des structs, écrivons un programme qui calcule l'aire d'un rectangle. Nous commencerons par utiliser des variables individuelles, puis refactoriser le programme jusqu'à ce que nous utilisions des structs à la place.

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

Nom de fichier : src/main.rs

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

    println!(
        "L'aire du rectangle est {} pixels carrés.",
        area(width1, height1)
    );
}

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

Liste 5-8 : Calcul de l'aire d'un rectangle spécifié par des variables de largeur et de hauteur séparées

Maintenant, exécutez ce programme avec cargo run :

L'aire du rectangle est 1500 pixels carrés.

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

Le problème de ce code est évident dans la signature de area :

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

La fonction area 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 clair nulle part dans notre programme que les 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à discuté d'une manière dont nous pourrions le faire dans "Le type tuple" : en utilisant des tuples.

Refactoring avec des tuples

La liste 5-9 montre une autre version de notre programme qui utilise des tuples.

Nom de fichier : src/main.rs

fn main() {
    let rect1 = (30, 50);

    println!(
        "L'aire du rectangle est {} pixels carrés.",
      1 area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
  2 dimensions.0 * dimensions.1
}

Liste 5-9 : Spécification de la largeur et de la hauteur du rectangle avec un tuple

D'une certaine manière, ce programme est meilleur. Les tuples nous permettent d'ajouter une certaine structure, et nous passons maintenant un seul argument [1]. Mais d'une autre manière, cette version est moins claire : les tuples ne nomment pas leurs éléments, donc nous devons accéder aux parties du tuple par index [2], ce qui rend notre calcul moins évident.

Mélanger la largeur et la hauteur n'aurait pas d'importance pour le calcul de l'aire, mais si nous voulons dessiner le rectangle sur l'écran, cela compterait! Nous devrions garder à l'esprit que width est l'index du tuple 0 et height est l'index du tuple 1. Cela serait encore plus difficile pour autrui de comprendre et de garder à l'esprit s'ils devaient utiliser notre code. Parce que nous n'avons pas transmis la signification de nos données dans notre code, il est maintenant plus facile d'introduire des erreurs.

Refactoring avec des structs : Ajout de plus de sens

Nous utilisons des structs pour ajouter du sens en étiquetant les données. Nous pouvons transformer le tuple que nous utilisons en un struct avec un nom pour l'ensemble ainsi que des noms pour les parties, comme montré dans la liste 5-10.

Nom de fichier : src/main.rs

1 struct Rectangle {
  2 width: u32,
    height: u32,
}

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

    println!(
        "L'aire du rectangle est {} pixels carrés.",
        area(&rect1)
    );
}

4 fn area(rectangle: &Rectangle) -> u32 {
  5 rectangle.width * rectangle.height
}

Liste 5-10 : Définition d'un struct Rectangle

Ici, nous avons défini un struct et l'avons nommé Rectangle [1]. Dans les accolades, nous avons défini les champs comme width et height, tous les deux de type u32 [2]. Ensuite, dans main, nous avons créé une instance particulière de Rectangle qui a une largeur de 30 et une hauteur de 50 [3].

Notre fonction area est maintenant définie avec un paramètre, que nous avons nommé rectangle, dont le type est un emprunt immuable d'une instance de struct Rectangle [4]. Comme mentionné au chapitre 4, nous voulons emprunter le struct plutôt que prendre sa propriété. De cette manière, main conserve sa propriété et peut continuer à utiliser rect1, qui est la raison pour laquelle nous utilisons le & dans la signature de la fonction et où nous appelons la fonction.

La fonction area accède aux champs width et height de l'instance de Rectangle [5] (remarquez 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 indique maintenant exactement ce que nous voulons dire : calculer l'aire de Rectangle, en utilisant ses champs width et height. Cela indique que la largeur et la hauteur sont liées l'une à l'autre, et il donne des noms descriptifs aux valeurs plutôt que d'utiliser les valeurs d'index de tuple de 0 et 1. C'est un gain en termes de clarté.

Ajout de fonctionnalités utiles avec des traits dérivés

Il serait utile de pouvoir afficher une instance de Rectangle pendant le débogage de notre programme et de voir les valeurs de tous ses champs. La liste 5-11 essaie d'utiliser la macro println! comme nous l'avons fait dans les chapitres précédents. Cela ne fonctionnera pas, cependant.

Nom de fichier : src/main.rs

struct Rectangle {
    width: u32,
    height: u32,
}

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

    println!("rect1 is {}", rect1);
}

Liste 5-11 : Tentative d'affichage d'une instance de Rectangle

Lorsque nous compilons ce code, nous obtenons une erreur avec ce message central :

error[E0277]: `Rectangle` n'implémente pas `std::fmt::Display`

La macro println! peut effectuer de nombreux types de formatage, et par défaut, les accolades indiquent à println! d'utiliser un formatage appelé Display : une sortie destinée à une consommation directe par l'utilisateur final. Les types primitifs que nous avons vus jusqu'à présent implémentent Display par défaut car il n'y a qu'un seul moyen de montrer un 1 ou tout autre type primitif à un utilisateur. Mais avec les structs, la manière 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 devraient-ils être affichés? En raison de cette ambiguïté, Rust ne tente pas de deviner ce que nous voulons, et les structs n'ont pas de mise en œuvre fournie de Display à utiliser avec println! et le placeholder {}.

Si nous continuons à lire les erreurs, nous trouverons cette note utile :

= help: le trait `std::fmt::Display` n'est pas implémenté pour `Rectangle`
= note: dans les chaînes de formatage, vous pouvez peut-être utiliser `{:?}` (ou {:#?} pour
l'affichage joli) à la place

Essayons-le! L'appel de macro println! ressemblera maintenant à println!("rect1 is {:?}", rect1);. En plaçant le spécificateur :? à l'intérieur des accolades, nous indiquons à 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 le débogage de notre code.

Compilez le code avec ce changement. Zut! Nous obtenons toujours une erreur :

error[E0277]: `Rectangle` n'implémente pas `Debug`

Mais encore une fois, le compilateur nous donne une note utile :

= help: le trait `Debug` n'est pas implémenté pour `Rectangle`
= note: ajoutez `#[derive(Debug)]` ou implémentez manuellement `Debug`

Rust inclut effectivement une fonctionnalité pour afficher des informations de débogage, mais nous devons explicitement choisir de rendre cette fonctionnalité disponible pour notre struct. Pour ce faire, nous ajoutons l'attribut externe #[derive(Debug)] juste avant la définition du struct, comme montré dans la liste 5-12.

Nom de fichier : src/main.rs

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

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

    println!("rect1 is {:?}", rect1);
}

Liste 5-12 : Ajout de l'attribut pour dériver le trait Debug et affichage de l'instance de Rectangle en utilisant le format de débogage

Maintenant, lorsque nous exécutons le programme, nous n'obtenons pas d'erreurs, et nous voyons la sortie suivante :

rect1 is Rectangle { width: 30, height: 50 }

Très bien! Ce n'est pas la sortie la plus jolie, mais elle montre les valeurs de tous les champs pour cette instance, ce qui serait certainement utile pendant le débogage. Lorsque nous avons des structs plus grands, il est utile d'avoir une sortie un peu plus facile à lire ; dans ces cas, nous pouvons utiliser {:#?} à la place de {:?} dans la chaîne println!. Dans cet exemple, en utilisant le style {:#?}, la sortie sera la suivante :

rect1 is Rectangle {
    width: 30,
    height: 50,
}

Une autre manière d'afficher une valeur au format Debug est d'utiliser la macro dbg!, qui prend la propriété d'une expression (contrairement à println!, qui prend une référence), affiche le nom du fichier et le numéro de ligne où cet appel de macro dbg! se produit dans votre code ainsi que la valeur résultante de cette expression, et renvoie la propriété de la valeur.

Note : Appeler la macro dbg! imprime dans le flux de console d'erreur standard (stderr), contrairement à println!, qui imprime dans le flux de console de sortie standard (stdout). Nous en parlerons plus en détail dans "Écriture de messages d'erreur sur la sortie d'erreur standard au lieu de la sortie standard".

Voici un exemple où nous sommes intéressés par la valeur qui est assignée au champ width, ainsi que la valeur du struct entier dans rect1 :

Nom de fichier : src/main.rs

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

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

  2 dbg!(&rect1);
}

Nous pouvons placer dbg! autour de l'expression 30 * scale [1] et, puisque dbg! renvoie la propriété de la valeur de l'expression, le champ width aura la même valeur que si nous n'avions pas le call dbg! là. Nous ne voulons pas que dbg! prenne la propriété de rect1, donc nous utilisons une référence à rect1 dans l'appel suivant [2]. Voici à quoi ressemble la sortie de cet exemple :

[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

Nous pouvons voir que le premier morceau de sortie provient de [1] où nous débuggeons l'expression 30 * scale, et sa valeur résultante est 60 (le formatage Debug implémenté pour les entiers est de n'afficher que leur valeur). L'appel dbg! à [2] affiche la valeur de &rect1, qui est le struct Rectangle. Cette sortie utilise le formatage Debug joli du type Rectangle. La macro dbg! peut être vraiment utile lorsque vous essayez de comprendre ce que fait votre code!

En plus du trait Debug, Rust a fourni un certain nombre de traits pour que nous puissions les utiliser avec l'attribut derive qui peuvent ajouter un comportement utile à nos types personnalisés. Ces traits et leurs comportements sont listés dans l'annexe C. Nous aborderons la manière d'implémenter ces traits avec un comportement personnalisé ainsi que la manière de créer vos propres traits au chapitre 10. Il existe également de nombreux attributs autres que derive ; pour plus d'informations, consultez la section "Attributs" de la référence Rust à https://doc.rust-lang.org/reference/attributes.html.

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 elle ne fonctionnera pas avec tout 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.

Sommaire

Félicitations! Vous avez terminé le laboratoire An Example Program Using Structs. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.