Chemins dans l'arborescence de modules Rust

Beginner

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

Introduction

Bienvenue dans Paths for Referring to an Item in the Module Tree. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous apprenons que les chemins sont utilisés en Rust pour faire référence à des éléments dans l'arborescence des modules, et peuvent prendre la forme de chemins absolus ou de chemins relatifs.

Paths for Referring to an Item in the Module Tree

Pour montrer à Rust où trouver un élément dans l'arborescence des modules, nous utilisons un chemin de la même manière que lorsque nous navigons dans un système de fichiers. Pour appeler une fonction, nous devons connaître son chemin.

Un chemin peut prendre deux formes :

  • Un chemin absolu est le chemin complet commençant par une racine de crate ; pour le code provenant d'un crate externe, le chemin absolu commence par le nom du crate, et pour le code du crate actuel, il commence par le littéral crate.
  • Un chemin relatif commence par le module actuel et utilise self, super ou un identifiant dans le module actuel.

Les chemins absolus et relatifs sont suivis d'un ou plusieurs identifiants séparés par deux points (::).

Revoyons la Liste 7-1. Disons que nous voulons appeler la fonction add_to_waitlist. C'est comme demander : quel est le chemin de la fonction add_to_waitlist? La Liste 7-3 contient la Liste 7-1 avec certains des modules et fonctions supprimés.

Nous montrerons deux façons d'appeler la fonction add_to_waitlist à partir d'une nouvelle fonction, eat_at_restaurant, définie dans la racine du crate. Ces chemins sont corrects, mais il reste un autre problème qui empêchera cet exemple de compiler tel quel. Nous expliquerons pourquoi un peu plus tard.

La fonction eat_at_restaurant est partie de l'API publique de notre crate de bibliothèque, donc nous la marquons avec le mot clé pub. Dans "Exposing Paths with the pub Keyword", nous approfondirons le sujet du pub.

Nom de fichier : src/lib.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Chemin absolu
    crate::front_of_house::hosting::add_to_waitlist();

    // Chemin relatif
    front_of_house::hosting::add_to_waitlist();
}

Liste 7-3 : Appel de la fonction add_to_waitlist en utilisant des chemins absolus et relatifs

La première fois que nous appelons la fonction add_to_waitlist dans eat_at_restaurant, nous utilisons un chemin absolu. La fonction add_to_waitlist est définie dans le même crate que eat_at_restaurant, ce qui signifie que nous pouvons utiliser le mot clé crate pour commencer un chemin absolu. Nous incluons ensuite chacun des modules successifs jusqu'à ce que nous arrivions à add_to_waitlist. Vous pouvez imaginer un système de fichiers avec la même structure : nous spécifierions le chemin /front_of_house/hosting/add_to_waitlist pour exécuter le programme add_to_waitlist ; utiliser le nom du crate pour commencer à partir de la racine du crate est comme utiliser / pour commencer à partir de la racine du système de fichiers dans votre shell.

La seconde fois que nous appelons add_to_waitlist dans eat_at_restaurant, nous utilisons un chemin relatif. Le chemin commence par front_of_house, le nom du module défini au même niveau de l'arborescence des modules que eat_at_restaurant. Ici, l'équivalent dans le système de fichiers serait d'utiliser le chemin front_of_house/hosting/add_to_waitlist. Commencer par un nom de module signifie que le chemin est relatif.

Choisir d'utiliser un chemin relatif ou absolu est une décision que vous prendrez en fonction de votre projet, et cela dépend de si vous êtes plus susceptible de déplacer le code de définition d'un élément séparément ou ensemble avec le code qui utilise l'élément. Par exemple, si nous déplacions le module front_of_house et la fonction eat_at_restaurant dans un module nommé customer_experience, nous devrions mettre à jour le chemin absolu vers add_to_waitlist, mais le chemin relatif serait toujours valide. Cependant, si nous déplacions la fonction eat_at_restaurant séparément dans un module nommé dining, le chemin absolu vers l'appel de add_to_waitlist resterait le même, mais le chemin relatif devrait être mis à jour. Notre préférence générale est de spécifier des chemins absolus car il est plus probable que nous voulions déplacer les définitions de code et les appels d'éléments indépendamment l'un de l'autre.

Essayons de compiler la Liste 7-3 et trouvons pourquoi elle ne compile pas encore! Les erreurs que nous obtenons sont montrées dans la Liste 7-4.

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
 --> src/lib.rs:9:28
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                            ^^^^^^^ private module
  |
note: the module `hosting` is defined here
 --> src/lib.rs:2:5
  |
2 |     mod hosting {
  |     ^^^^^^^^^^^

error[E0603]: module `hosting` is private
  --> src/lib.rs:12:21
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                     ^^^^^^^ private module
   |
note: the module `hosting` is defined here
  --> src/lib.rs:2:5
   |
2  |     mod hosting {
   |     ^^^^^^^^^^^

Liste 7-4 : Erreurs du compilateur lors de la construction du code de la Liste 7-3

Les messages d'erreur disent que le module hosting est privé. En d'autres termes, nous avons les chemins corrects pour le module hosting et la fonction add_to_waitlist, mais Rust ne nous permet pas de les utiliser car il n'a pas accès aux parties privées. En Rust, tous les éléments (fonctions, méthodes, structs, enums, modules et constantes) sont privés par défaut pour les modules parents. Si vous voulez rendre un élément tel qu'une fonction ou un struct privé, vous le mettez dans un module.

Les éléments dans un module parent ne peuvent pas utiliser les éléments privés à l'intérieur des modules enfants, mais les éléments dans les modules enfants peuvent utiliser les éléments dans leurs modules ancêtres. C'est parce que les modules enfants enveloppent et cachent leurs détails d'implémentation, mais les modules enfants peuvent voir le contexte dans lequel ils sont définis. Pour continuer avec notre métaphore, imaginez les règles de confidentialité comme étant comme le bureau arrière d'un restaurant : ce qui se passe là-dedans est privé pour les clients du restaurant, mais les gestionnaires de bureau peuvent voir et faire tout dans le restaurant qu'ils gèrent.

Rust a choisi de faire fonctionner le système de modules de cette manière afin que le fait de cacher les détails d'implémentation interne soit la norme. Ainsi, vous savez quelles parties du code interne vous pouvez modifier sans casser le code externe. Cependant, Rust vous donne l'option d'exposer les parties internes du code des modules enfants aux modules ancêtres externes en utilisant le mot clé pub pour rendre un élément public.

Exposing Paths with the pub Keyword

Revoyons l'erreur de la Liste 7-4 qui nous a dit que le module hosting est privé. Nous voulons que la fonction eat_at_restaurant dans le module parent ait accès à la fonction add_to_waitlist dans le module enfant, donc nous marquons le module hosting avec le mot clé pub, comme montré dans la Liste 7-5.

Nom de fichier : src/lib.rs

mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist() {}
    }
}

--snip--

Liste 7-5 : Décaration du module hosting comme pub pour l'utiliser à partir de eat_at_restaurant

Malheureusement, le code de la Liste 7-5 entraîne toujours des erreurs de compilation, comme montré dans la Liste 7-6.

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
 --> src/lib.rs:9:37
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                                     ^^^^^^^^^^^^^^^ private function
  |
note: the function `add_to_waitlist` is defined here
 --> src/lib.rs:3:9
  |
3 |         fn add_to_waitlist() {}
  |         ^^^^^^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:12:30
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                              ^^^^^^^^^^^^^^^ private function
   |
note: the function `add_to_waitlist` is defined here
  --> src/lib.rs:3:9
   |
3  |         fn add_to_waitlist() {}
   |         ^^^^^^^^^^^^^^^^^^^^

Liste 7-6 : Erreurs du compilateur lors de la construction du code de la Liste 7-5

Qu'est-ce qui s'est passé? Ajouter le mot clé pub devant mod hosting rend le module public. Avec ce changement, si nous pouvons accéder à front_of_house, nous pouvons accéder à hosting. Mais les contenus de hosting restent privés ; rendre le module public ne rend pas ses contenus publics. Le mot clé pub sur un module permet seulement au code dans ses modules ancêtres de se référer à lui, pas d'accéder à son code interne. Comme les modules sont des conteneurs, il n'y a pas grand-chose que nous puissions faire en ne rendant que le module public ; nous devons aller plus loin et choisir de rendre public un ou plusieurs des éléments à l'intérieur du module également.

Les erreurs de la Liste 7-6 disent que la fonction add_to_waitlist est privée. Les règles de confidentialité s'appliquent aux structs, enums, fonctions, méthodes ainsi qu'aux modules.

Rendons également la fonction add_to_waitlist publique en ajoutant le mot clé pub avant sa définition, comme dans la Liste 7-7.

Nom de fichier : src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

--snip--

Liste 7-7 : Ajout du mot clé pub à mod hosting et fn add_to_waitlist nous permet d'appeler la fonction à partir de eat_at_restaurant.

Maintenant, le code compilera! Pour voir pourquoi ajouter le mot clé pub nous permet d'utiliser ces chemins dans add_to_waitlist par rapport aux règles de confidentialité, regardons les chemins absolus et relatifs.

Dans le chemin absolu, nous commençons par crate, la racine de l'arborescence de modules de notre crate. Le module front_of_house est défini dans la racine du crate. Bien que front_of_house ne soit pas public, puisque la fonction eat_at_restaurant est définie dans le même module que front_of_house (c'est-à-dire que eat_at_restaurant et front_of_house sont des frères), nous pouvons nous référer à front_of_house à partir de eat_at_restaurant. Ensuite vient le module hosting marqué avec pub. Nous pouvons accéder au module parent de hosting, donc nous pouvons accéder à hosting. Enfin, la fonction add_to_waitlist est marquée avec pub et nous pouvons accéder à son module parent, donc cet appel de fonction fonctionne!

Dans le chemin relatif, la logique est la même que pour le chemin absolu, sauf pour la première étape : au lieu de commencer à partir de la racine du crate, le chemin commence par front_of_house. Le module front_of_house est défini dans le même module que eat_at_restaurant, donc le chemin relatif commençant par le module dans lequel eat_at_restaurant est définie fonctionne. Ensuite, puisque hosting et add_to_waitlist sont marqués avec pub, le reste du chemin fonctionne, et cet appel de fonction est valide!

Si vous prévoyez de partager votre crate de bibliothèque afin que d'autres projets puissent utiliser votre code, votre API publique est votre contrat avec les utilisateurs de votre crate qui détermine la manière dont ils peuvent interagir avec votre code. Il y a de nombreuses considérations autour de la gestion des modifications de votre API publique pour faciliter la dépendance des gens à votre crate. Ces considérations sont en dehors des limites de ce livre ; si vous êtes intéressé par ce sujet, consultez les Rust API Guidelines à https://rust-lang.github.io/api-guidelines.

Best Practices for Packages with a Binary and a Library

Nous avons mentionné qu'un package peut contenir à la fois une racine de crate binaire src/main.rs ainsi qu'une racine de crate de bibliothèque src/lib.rs, et que les deux crates auront le nom du package par défaut. En général, les packages avec ce modèle de conteneur à la fois une bibliothèque et une crate binaire auront assez de code dans la crate binaire pour démarrer un exécutable qui appelle du code avec la crate de bibliothèque. Cela permet à d'autres projets de profiter de la plupart des fonctionnalités que le package offre car le code de la crate de bibliothèque peut être partagé.

L'arborescence de modules devrait être définie dans src/lib.rs. Ensuite, tout élément public peut être utilisé dans la crate binaire en commençant les chemins par le nom du package. La crate binaire devient un utilisateur de la crate de bibliothèque tout comme une crate complètement externe utiliserait la crate de bibliothèque : elle ne peut utiliser que l'API publique. Cela vous aide à concevoir une bonne API ; non seulement êtes-vous l'auteur, vous êtes également un client!

Dans le Chapitre 12, nous démontrerons cette pratique d'organisation avec un programme de ligne de commande qui contiendra à la fois une crate binaire et une crate de bibliothèque.

Starting Relative Paths with super

Nous pouvons construire des chemins relatifs qui commencent dans le module parent, plutôt que dans le module actuel ou la racine du crate, en utilisant super au début du chemin. C'est comme commencer un chemin de système de fichiers avec la syntaxe ... Utiliser super nous permet de faire référence à un élément que nous savons se trouver dans le module parent, ce qui peut faciliter le réarrangement de l'arborescence de modules lorsque le module est étroitement lié au parent, mais que le parent pourrait être déplacé ailleurs dans l'arborescence de modules un jour.

Considérez le code de la Liste 7-8 qui modélise la situation dans laquelle un chef corrige une commande incorrecte et l'apporte personnellement au client. La fonction fix_incorrect_order définie dans le module back_of_house appelle la fonction deliver_order définie dans le module parent en spécifiant le chemin vers deliver_order, en commençant par super.

Nom de fichier : src/lib.rs

fn deliver_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::deliver_order();
    }

    fn cook_order() {}
}

Liste 7-8 : Appel d'une fonction en utilisant un chemin relatif commençant par super

La fonction fix_incorrect_order est dans le module back_of_house, donc nous pouvons utiliser super pour aller au module parent de back_of_house, qui dans ce cas est crate, la racine. À partir de là, nous cherchons deliver_order et nous le trouvons. Succès! Nous pensons que le module back_of_house et la fonction deliver_order sont susceptibles de rester dans la même relation l'une avec l'autre et d'être déplacés ensemble si nous décidons de reorganiser l'arborescence de modules du crate. Par conséquent, nous avons utilisé super pour avoir moins de places à mettre à jour le code à l'avenir si ce code est déplacé dans un autre module.

Making Structs and Enums Public

Nous pouvons également utiliser pub pour désigner des structs et des enums comme publics, mais il y a quelques détails supplémentaires concernant l'utilisation de pub avec des structs et des enums. Si nous utilisons pub avant la définition d'un struct, nous rendons le struct public, mais les champs du struct resteront privés. Nous pouvons rendre chacun des champs publics ou non au cas par cas. Dans la Liste 7-9, nous avons défini un struct public back_of_house::Breakfast avec un champ public toast mais un champ privé seasonal_fruit. Cela modélise le cas dans un restaurant où le client peut choisir le type de pain qui accompagne un repas, mais le chef décide de quel fruit accompagner le repas en fonction de ce qui est de saison et en stock. Les fruits disponibles changent rapidement, donc les clients ne peuvent pas choisir le fruit ou même voir lequel fruit ils vont recevoir.

Nom de fichier : src/lib.rs

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // Change our mind about what bread we'd like
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not
    // allowed to see or modify the seasonal fruit that comes
    // with the meal
    // meal.seasonal_fruit = String::from("blueberries");
}

Liste 7-9 : Un struct avec certains champs publics et certains champs privés

Parce que le champ toast dans le struct back_of_house::Breakfast est public, dans eat_at_restaurant nous pouvons lire et écrire dans le champ toast en utilisant la notation pointée. Remarquez que nous ne pouvons pas utiliser le champ seasonal_fruit dans eat_at_restaurant, car seasonal_fruit est privé. Essayez de décommenter la ligne modifiant la valeur du champ seasonal_fruit pour voir quel message d'erreur vous obtenez!

De plus, notez que parce que back_of_house::Breakfast a un champ privé, le struct doit fournir une fonction associée publique qui construit une instance de Breakfast (nous l'avons nommée summer ici). Si Breakfast n'avait pas une telle fonction, nous ne pourrions pas créer une instance de Breakfast dans eat_at_restaurant car nous ne pourrions pas définir la valeur du champ privé seasonal_fruit dans eat_at_restaurant.

En revanche, si nous rendons un enum public, toutes ses variantes sont alors publiques. Nous n'avons besoin que du mot clé pub avant le mot clé enum, comme montré dans la Liste 7-10.

Nom de fichier : src/lib.rs

mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

Liste 7-10 : Désigner un enum comme public rend toutes ses variantes publiques.

Parce que nous avons rendu l'enum Appetizer public, nous pouvons utiliser les variantes Soup et Salad dans eat_at_restaurant.

Les enums ne sont pas très utiles à moins que leurs variantes ne soient publiques ; il serait ennuyeux d'avoir à annoter toutes les variantes d'enum avec pub dans chaque cas, donc la valeur par défaut pour les variantes d'enum est d'être publiques. Les structs sont souvent utiles sans que leurs champs soient publics, donc les champs de struct suivent la règle générale selon laquelle tout est privé par défaut, sauf s'il est annoté avec pub.

Il y a encore une situation impliquant pub que nous n'avons pas couverte, et c'est notre dernière fonctionnalité de système de modules : le mot clé use. Nous aborderons use tout seul d'abord, puis nous montrerons comment combiner pub et use.

Summary

Félicitations! Vous avez terminé le labo Paths for Referring to an Item in the Module Tree. Vous pouvez pratiquer d'autres labs dans LabEx pour améliorer vos compétences.