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

Importer des chemins dans la portée via le mot-clé use

Devoir écrire les chemins pour appeller des fonctions peut paraître pénible et répétitif. Dans l’encart 7-7, que nous ayons choisi d’utiliser le chemin absolu ou relatif pour la fonction ajouter_a_la_liste_attente, nous aurions dû aussi écrire salle_a_manger et accueil à chaque fois que nous voulions appeler ajouter_a_la_liste_attente. Heureusement, il existe une solution pour simplifier ce cheminement : nous pouvons créer un raccourci vers un chemin avec le mot-clé use et ensuite utiliser le nom court partout ailleurs dans la portée.

Dans l’encart 7-11, nous importons le module crate::salle_a_manger::accueil dans la portée de la fonction manger_au_restaurant afin que nous n’ayons plus qu’à utiliser accueil::ajouter_a_la_liste_attente pour appeler la fonction ajouter_a_la_liste_attente dans manger_au_restaurant.

Filename: src/lib.rs
mod salle_a_manger {
    pub mod accueil {
        pub fn ajouter_a_la_liste_attente() {}
    }
}

use crate::salle_a_manger::accueil;

pub fn manger_au_restaurant() {
    accueil::ajouter_a_la_liste_attente();
}
Listing 7-11: Bringing a module into scope with use

Dans une portée, utiliser un use et un chemin s’apparente à créer un lien symbolique dans le système de fichiers. Grâce à l’ajout de use crate::salle_a_manger::accueil à la racine de la crate, accueil est maintenant un nom valide dans cette portée, comme si le module accueil avait été défini à la racine de la crate. Les chemins importés dans la portée via use doivent respecter les règles de visibilité, tout comme les autres chemins.

Notez que use se contente de créer le raccourci uniquement pour la portée dans laquelle se trouve le use. L’encart 7-12 déplace la fonction manger_au_restaurant dans un nouveau module enfant appelé client, qui se trouve dans une portée différente de celle de l’instruction du use, de telle sorte que le corps de la fonction ne pourra pas être compilé.

Filename: src/lib.rs
mod salle_a_manger {
    pub mod accueil {
        pub fn ajouter_a_la_liste_attente() {}
    }
}

use crate::salle_a_manger::accueil;

mod client {
    pub fn manger_au_restaurant() {
        accueil::ajouter_a_la_liste_attente();
    }
}
Listing 7-12: A use statement only applies in the scope it’s in.

L’erreur du compilateur montre que le raccourci ne s’applique plus à l’intérieur du module client :

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0433]: failed to resolve: use of unresolved module or unlinked crate `accueil`
  --> src/lib.rs:11:9
   |
11 |         accueil::ajouter_a_la_liste_attente();
   |         ^^^^^^^ use of unresolved module or unlinked crate `accueil`
   |
   = help: if you wanted to use a crate named `accueil`, use `cargo add accueil` to add it to your `Cargo.toml`
help: consider importing this module through its public re-export
   |
10 +     use crate::accueil;
   |

warning: unused import: `crate::salle_a_manger::accueil`
 --> src/lib.rs:7:5
  |
7 | use crate::salle_a_manger::accueil;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` (lib) due to 1 previous error; 1 warning emitted

Notez qu’il y a aussi un avertissement selon lequel le use n’est plus utilisé dans cette portée ! Pour solutionner ce problème, il faut aussi déplacer le use dans le module client, ou bien référencer le raccourci dans le module parent avec super::accueil dans le module enfant client.

Créer des chemins idéaux pour use

Dans l’encart 7-11, vous vous êtes peut-être demandé pourquoi nous avions utilisé use crate::salle_a_manger::accueil et appelé ensuite accueil::ajouter_a_la_liste_attente dans manger_au_restaurant plutôt que d’écrire le chemin du use jusqu’à la fonction ajouter_a_la_liste_attente pour avoir le même résultat, comme dans l’encart 7-13.

Filename: src/lib.rs
mod salle_a_manger {
    pub mod accueil {
        pub fn ajouter_a_la_liste_attente() {}
    }
}

use crate::salle_a_manger::accueil::ajouter_a_la_liste_attente;

pub fn manger_au_restaurant() {
    ajouter_a_la_liste_attente();
}
Listing 7-13: Bringing the add_to_waitlist function into scope with use, which is unidiomatic

Bien que l’encart 7-11 et 7-13 accomplissent la même tâche, l’encart 7-11 est la façon idéale d’importer une fonction dans la portée via use. L’import du module parent de la fonction dans notre portée avec use nécessite que nous ayons à préciser le module parent quand nous appelons la fonction. Renseigner le module parent lorsque nous appelons la fonction précise clairement que la fonction n’est pas définie localement, tout en minimisant la répétition du chemin complet. Nous ne pouvons pas repérer facilement là où est défini ajouter_a_la_liste_attente dans l’encart 7-13.

Cela dit, lorsque nous importons des structures, des énumérations et d’autres éléments avec use, il est idéal de préciser le chemin complet. L’encart 7-14 montre la manière idéale d’importer la structure HashMap de la bibliothèque standard dans la portée d’une crate binaire.

Filename: src/main.rs
use std::collections::HashMap;

fn main() {
    let mut table = HashMap::new();
    map.insert(1, 2);
}
Listing 7-14: Bringing HashMap into scope in an idiomatic way

Il n’y a pas de forte justification à cette pratique : c’est simplement une convention qui a germé, et les gens se sont habitués à lire et écrire du code Rust de cette façon.

Il y a une exception à cette pratique : nous ne pouvons pas utiliser l’instruction use pour importer deux éléments avec le même nom dans la portée, car Rust ne l’autorise pas. L’encart 7-15 nous montre comment importer puis utiliser deux types Result ayant le même nom mais dont les modules parents sont distincts.

Filename: src/lib.rs
use std::fmt;
use std::io;

fn fonction1() -> fmt::Result {
    // -- partie masquée ici --
    Ok(())
}

fn fonction2() -> io::Result<()> {
    // -- partie masquée ici --
    Ok(())
}
Listing 7-15: Bringing two types with the same name into the same scope requires using their parent modules.

Comme vous pouvez le constater, l’utilisation des modules parents permet de distinguer les deux types Result. Si nous avions utilisé use std::fmt::Result et use std::io::Result, nous aurions deux types nommés Result dans la même portée et donc Rust ne pourrait pas comprendre lequel nous voudrions utiliser en demandant Result.

Renommer des éléments avec le mot-clé as

Il y a une autre solution au fait d’avoir deux types du même nom dans la même portée à cause de use : après le chemin, nous pouvons rajouter as suivi d’un nouveau nom local, ou alias, sur le type. L’encart 7-16 nous montre une autre façon d’écrire le code de l’encart 7-15 en utilisant as pour renommer un des deux types Result.

Filename: src/lib.rs
use std::fmt::Result;
use std::io::Result as IoResult;

fn fonction1() -> Result {
    // -- partie masquée ici --
    Ok(())
}

fn fonction2() -> IoResult<()> {
    // -- partie masquée ici --
    Ok(())
}
Listing 7-16: Renaming a type when it’s brought into scope with the as keyword

Dans la seconde instruction use, nous avons choisi IoResult comme nouveau nom du type std::io::Result, qui n’est plus en conflit avec le Result de std::fmt que nous avons aussi importé dans la portée. Les encarts 7-15 et 7-16 sont idéaux, donc le choix vous revient !

Réexporter des éléments avec pub use

Lorsque nous importons un élément dans la portée avec le mot-clé use, son nom dans la portée dans laquelle nous l’avons importé est privé. Pour permettre à du code extérieur d’utiliser ce nom comme s’il était défini dans cette portée, nous pouvons associer pub et use. Cette technique est appelée réexporter car nous importons un élément dans la portée, mais nous rendons aussi cet élément disponible aux portées des autres.

L’encart 7-17 nous montre le code de l’encart 7-11 où le use du module racine a été remplacé par pub use.

Filename: src/lib.rs
mod salle_a_manger {
    pub mod accueil {
        pub fn ajouter_a_la_liste_attente() {}
    }
}

pub use crate::salle_a_manger::accueil;

pub fn manger_au_restaurant() {
    accueil::ajouter_a_la_liste_attente();
}
Listing 7-17: Making a name available for any code to use from a new scope with pub use

Avant ce changement, un code extérieur aurait dû appeler la fonction ajouter_a_la_liste_attente en utilisant le chemin restaurant::salle_a_manger::accueil::ajouter_a_la_liste_attente(), ce qui aurait aussi requis que le module salle_a_manger soit marqué comme pub. Maintenant que ce pub use a réexporté le module accueil depuis le module racine, le code extérieur peut utiliser le chemin restaurant::salle_a_manger::ajouter_a_la_liste_attente() à la place.

Réexporter est utile quand la structure interne de votre code est différente de la façon dont les développeurs qui utilisent votre code se la représentent. Par exemple, dans cette métaphore du restaurant, les personnes qui font fonctionner le restaurant se structurent en fonction de la “salle à manger” et des “cuisines”. Mais les clients qui utilisent le restaurant ne vont probablement pas voir les choses ainsi. Avec pub use, nous pouvons écrire notre code selon une certaine organisation, mais l’exposer avec une organisation différente. En faisant ainsi, la bibliothèque est bien organisée autant pour les développeurs qui travaillent sur la bibliothèque que pour les développeurs qui utilisent la bibliothèque. Nous verrons un autre exemple de pub use et comment il affecte la documentation de la crate dans « Exporter une API publique conviviale » dans le chapitre 14.

Utiliser des paquets externes

Dans le chapitre 2, nous avions développé un projet de jeu du plus ou du moins qui utilisait le paquet externe rand afin d’obtenir des nombres aléatoires. Pour pouvoir utiliser rand dans notre projet, nous avions ajouté cette ligne dans Cargo.toml :

Filename: Cargo.toml
rand = "0.8.5"

L’ajout de rand comme dépendance dans Cargo.toml demande à Cargo de télécharger le paquet rand et toutes ses dépendances à partir de crates.io et rend disponible rand pour notre projet.

Ensuite, pour importer les définitions de rand dans la portée de notre paquet, nous avions ajouté une ligne use qui commence avec le nom de la crate, rand, et nous avions listé les éléments que nous voulions importer dans notre portée. Dans la section “Générer le nombre secret” du chapitre 2, nous avions importé le trait Rng dans la portée, puis nous avions appelé la fonction rand::thread_rng :

use std::io;

use rand::Rng;

fn main() {
    println!("Devinez le nombre !");

    let nombre_secret = rand::thread_rng().gen_range(1..=100);

    println!("Le nombre secret est : {nombre_secret}");

    println!("Veuillez entrer un nombre.");

    let mut supposition = String::new();

    io::stdin()
        .read_line(&mut supposition)
        .expect("Échec de la lecture de l'entrée utilisateur");

    println!("Votre nombre : {supposition}");
}

Les membres de la communauté Rust ont mis à disposition de nombreux paquets sur crates.io, et utiliser l’un d’entre eux dans votre paquet implique toujours ces mêmes étapes : les lister dans le fichier Cargo.toml de votre paquet et utiliser use pour importer certains éléments de ces crates dans la portée.

Notez que la bibliothèque standard std est aussi une crate qui est externe à notre paquet. Comme la bibliothèque standard est livrée avec le langage Rust, nous n’avons pas à modifier le Cargo.toml pour y inclure std. Mais nous devons utiliser use pour importer les éléments qui se trouvent dans la portée de notre paquet. Par exemple, pour HashMap, nous pourrions utiliser cette ligne :

#![allow(unused)]
fn main() {
use std::collections::HashMap;
}

C’est un chemin absolu qui commence par std, le nom de la crate de la bibliothèque standard.

Utiliser des chemins imbriqués pour simplifier les listes de use

Si vous utilisez de nombreux éléments définis dans une même crate ou dans un même module, lister chaque élément sur sa propre ligne prendra beaucoup d’espace vertical dans vos fichiers. Par exemple, ces deux instructions use, que nous avions dans le jeu du plus ou du moins dans l’encart 2-4, importaient des éléments de std dans la portée :

Filename: src/main.rs
use rand::Rng;
// -- partie masquée ici --
use std::cmp::Ordering;
use std::io;
// -- partie masquée ici --

fn main() {
    println!("Devinez le nombre !");

    let nombre_secret = rand::thread_rng().gen_range(1..=100);

    println!("Le nombre secret est : {nombre_secret}");

    println!("Veuillez entrer un nombre.");

    let mut supposition = String::new();

    io::stdin()
        .read_line(&mut supposition)
        .expect("Échec de la lecture de l'entrée utilisateur");

    println!("Votre nombre : {supposition}");

    match supposition.cmp(&nombre_secret) {
        Ordering::Less => println!("C'est plus !"),
        Ordering::Greater => println!("C'est moins !"),
        Ordering::Equal => println!("Vous avez gagné !"),
    }
}

À la place, nous pouvons utiliser des chemins imbriqués afin d’importer ces mêmes éléments dans la portée en une seule ligne. Nous pouvons faire cela en indiquant la partie commune du chemin, suivi d’un double deux-points, puis d’accolades autour d’une liste des éléments qui diffèrent entre les chemins, comme dans l’encart 7-18 :

Filename: src/main.rs
use rand::Rng;
// -- partie masquée ici --
use std::{cmp::Ordering, io};
// -- partie masquée ici --

fn main() {
    println!("Devinez le nombre !");

    let nombre_secret = rand::thread_rng().gen_range(1..=100);

    println!("Le nombre secret est : {nombre_secret}");

    println!("Veuillez entrer un nombre.");

    let mut supposition = String::new();

    io::stdin()
        .read_line(&mut supposition)
        .expect("Échec de la lecture de l'entrée utilisateur");

    let supposition: u32 = supposition.trim().parse().expect("Veuillez entrer un nombre !");

    println!("Votre nombre : {supposition}");

    match supposition.cmp(&nombre_secret) {
        Ordering::Less => println!("C'est plus !"),
        Ordering::Greater => println!("C'est moins !"),
        Ordering::Equal => println!("Vous avez gagné !"),
    }
}
Listing 7-18: Specifying a nested path to bring multiple items with the same prefix into scope

Pour des programmes plus gros, importer plusieurs éléments dans la portée depuis la même crate ou module en utilisant des chemins imbriqués peut réduire considérablement le nombre de use utilisés !

Nous pouvons utiliser un chemin imbriqué à tous les niveaux d’un chemin, ce qui peut être utile lorsqu’on utilise deux instructions use qui partagent un sous-chemin. Par exemple, l’encart 7-19 nous montre deux instructions use : une qui importe std::io dans la portée et une autre qui importe std::io::Write dans la portée.

Filename: src/lib.rs
use std::io;
use std::io::Write;
Listing 7-19: Two use statements where one is a subpath of the other

La partie commune entre ces deux chemins est std::io, et c’est le premier chemin complet. Pour imbriquer ces deux chemins en une seule instruction use, nous pouvons utiliser self dans le chemin imbriqué, comme dans l’encart 7-20.

Filename: src/lib.rs
use std::io::{self, Write};
Listing 7-20: Combining the paths in Listing 7-19 into one use statement

Cette ligne importe std::io et std::io::Write dans la portée.

Import d’éléments avec l’opérateur global

Si nous voulons importer, dans la portée, tous les éléments publics définis dans un chemin, nous pouvons indiquer ce chemin suivi par l’opérateur global * :

#![allow(unused)]
fn main() {
use std::collections::*;
}

Cette instruction use va importer tous les éléments publics définis dans std::collections dans la portée courante. Mais soyez prudent quand vous utilisez l’opérateur global ! L’utilisation de l’opérateur global peut rendre difficile l’identification des noms dans la portée et là où un élément utilisé dans notre programme a été défini.

L’opérateur global est souvent utilisé lorsque nous écrivons des tests, pour importer tout ce qu’il y a à tester dans le module tests ; nous verrons cela dans une section du chapitre 11. L’opérateur global est parfois aussi utilisé pour l’étape préliminaire : rendez-vous dans la documentation de la bibliothèque standard pour plus d’informations sur cela.