Importer des chemins dans la portée via le mot-clé use
Les chemins que nous avons écrits jusqu'ici peuvent paraître pénibles car trop
longs et répétitifs. Par exemple, 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 importer un chemin dans la portée et appeler ensuite les éléments
de ce chemin comme s'ils étaient locaux grâce au mot-clé use
.
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
.
Fichier : 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();
accueil::ajouter_a_la_liste_attente();
accueil::ajouter_a_la_liste_attente();
}
Dans une portée, utiliser un use
et un chemin s'apparente à créer un lien
symbolique dans le système de fichier. 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.
Vous pouvez aussi importer un élément dans la portée avec use
et un chemin
relatif. L'encart 7-12 nous montre comment utiliser un chemin relatif pour
obtenir le même résultat que l'encart 7-11.
Fichier : src/lib.rs
mod salle_a_manger {
pub mod accueil {
pub fn ajouter_a_la_liste_attente() {}
}
}
use salle_a_manger::accueil;
pub fn manger_au_restaurant() {
accueil::ajouter_a_la_liste_attente();
accueil::ajouter_a_la_liste_attente();
accueil::ajouter_a_la_liste_attente();
}
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.
Fichier : 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();
ajouter_a_la_liste_attente();
ajouter_a_la_liste_attente();
}
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.
Fichier : src/main.rs
use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); }
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.
Fichier : 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(())
}
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
.
Fichier : 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(())
}
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 nouvelle portée est privé. Pour permettre au code appelant 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
.
Fichier : 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();
accueil::ajouter_a_la_liste_attente();
accueil::ajouter_a_la_liste_attente();
}
Grâce à pub use
, le code externe peut maintenant appeler la fonction
ajouter_a_la_liste_attente
en utilisant accueil::ajouter_a_la_liste_attente
.
Si nous n'avions pas utilisé pub use
, la fonction manger_au_restaurant
aurait pu appeler accueil::ajouter_a_la_liste_attente
dans sa portée, mais le
code externe n'aurait pas pu profiter de ce nouveau chemin.
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.
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 :
Fichier : Cargo.toml
rand = "0.8.3"
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..101);
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 grandes 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 :
Fichier : 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..101);
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 :
Fichier : 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..101);
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 saisir 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é !"),
}
}
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.
Fichier : src/lib.rs
use std::io;
use std::io::Write;
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.
Fichier : src/lib.rs
use std::io::{self, Write};
Cette ligne importe std::io
et std::io::Write
dans la portée.
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'opérateur global rend difficile à dire quels
éléments sont 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.