10. Modules, crates et workspaces
Exemple complet : examples/l10_modules.rs — cargo run --example l10_modules.
Workspace : l10-workspace/ — cargo run -p app depuis ce dossier.
Vocabulaire
Section intitulée « Vocabulaire »| Rust | C# | Java |
|---|---|---|
module (mod) |
namespace | package |
| crate — une unité de compilation, bibliothèque ou exécutable | assembly (.dll / .exe) |
JAR |
package — un Cargo.toml qui construit une ou plusieurs crates |
projet (.csproj) |
module Maven/Gradle |
| workspace — plusieurs packages construits ensemble | solution (.sln) |
build multi-modules (POM parent) |
| crates.io | NuGet | Maven Central |
Deux différences ressortent. Un namespace C# n’est qu’un préfixe de nommage et n’importe quel fichier peut y ajouter des éléments ; un module Rust est une véritable portée avec sa propre confidentialité, et l’arbre des modules est déclaré explicitement. Et en C#, chaque fichier .cs du dossier du projet est compilé ; en Rust, un fichier n’est compilé que si un module le déclare.
Modules et confidentialité
Section intitulée « Modules et confidentialité »mod billing { pub fn invoice_total(amounts: &[f64]) -> f64 { let subtotal: f64 = amounts.iter().sum(); subtotal + tax(subtotal) }
fn tax(amount: f64) -> f64 { amount * rates::SALES_TAX }
pub mod rates { pub const SALES_TAX: f64 = 0.15; }}
println!("{}", billing::tax(100.0));error[E0603]: function `tax` is private --> e10_private_fn.rs:12:29 |12 | println!("{}", billing::tax(100.0)); | ^^^ private function |note: the function `tax` is defined here --> e10_private_fn.rs:6:5 | 6 | fn tax(amount: f64) -> f64 { | ^^^^^^^^^^^^^^^^^^^^^^^^^^Tout est privé par défaut. Un élément privé est visible dans le module qui le définit et dans les enfants de ce module — rates pourrait donc appeler tax via super::tax, mais main, en dehors de billing, ne le peut pas.
| Rust | Visible depuis | C# | Java |
|---|---|---|---|
| (rien) | ce module et ses enfants | private |
private |
pub(super) |
le module parent aussi | — | — |
pub(crate) |
toute la crate | internal |
package-private (à peu près) |
pub |
partout où le module parent est visible | public |
public |
Il n’y a pas de protected : Rust n’a pas d’héritage entre structs.
Les champs d’une struct sont privés séparément
Section intitulée « Les champs d’une struct sont privés séparément »pub struct rend le type public, pas ses champs. C’est ainsi que Rust garantit les invariants — l’équivalent d’une classe avec des champs privés et un constructeur :
mod shapes { pub struct Rect { width: f64, height: f64, }
impl Rect { pub fn new(width: f64, height: f64) -> Self { Rect { width: width.max(0.0), height: height.max(0.0) } }
pub fn area(&self) -> f64 { self.width * self.height } }}
let r = shapes::Rect { width: 2.0, height: 3.0 };error[E0451]: fields `width` and `height` of struct `Rect` are private --> e10_private_literal.rs:19:28 |19 | let r = shapes::Rect { width: 2.0, height: 3.0 }; | ^^^^^ ^^^^^^ private field | | | private fieldLire r.width depuis l’extérieur échoue de la même manière (E0616: field width of struct Rect is private). Le code extérieur doit passer par Rect::new, qui ramène les tailles négatives à zéro. Les variantes d’un pub enum, en revanche, sont toujours publiques.
Chemins et use
Section intitulée « Chemins et use »use billing::discounts::with_discount; // amène une fonction dans la portéeuse billing::rates::SALES_TAX as TAX; // renomme, comme `using X = …`use shapes::Rect;
mod billing { // … pub mod discounts { pub fn with_discount(amounts: &[f64], percent: f64) -> f64 { super::invoice_total(amounts) * (1.0 - percent / 100.0) // module parent } }}
println!("{}", crate::billing::rates::SALES_TAX * 100.0); // chemin absolu depuis la racine de la cratetotal: 172.50with 10% off: 155.25tax rate: 0.15clamped area: 0unit square: 115| Début du chemin | Signifie | Comme |
|---|---|---|
crate:: |
la racine de la crate courante | global:: en C# |
super:: |
le module parent | .. dans un chemin de fichier |
self:: |
le module courant | . |
un nom de crate (std::, geometry::) |
une crate externe | l’espace de noms racine d’une assembly |
use ne fait que créer un raccourci ; sans lui, vous devez écrire le chemin complet. Le compilateur suggère l’import manquant :
error[E0422]: cannot find struct, variant or union type `Rect` in this scope --> e10_no_use.rs:8:13 |8 | let r = Rect { width: 2.0 }; | ^^^^ not found in this scope |help: consider importing this struct |1 + use crate::shapes::Rect; |pub use réexporte un nom, afin que les appelants voient un chemin plus simple que votre organisation interne — la bibliothèque de la section suivante s’en sert.
Des modules dans des fichiers
Section intitulée « Des modules dans des fichiers »mod shapes; avec un point-virgule au lieu d’un corps { … } indique au compilateur de charger le module depuis un fichier :
| Déclaration | Dans le fichier | Le corps du module se trouve dans |
|---|---|---|
mod shapes; |
src/lib.rs ou src/main.rs |
src/shapes.rs (ou l’ancien src/shapes/mod.rs) |
pub mod http; |
src/network.rs |
src/network/http.rs |
Le fichier ne contient que le corps — pas d’enveloppe mod shapes { }. Un fichier .rs qu’aucune déclaration mod n’atteint est tout simplement ignoré, même s’il se trouve dans src/.
Crates et packages
Section intitulée « Crates et packages »Un package peut contenir une crate bibliothèque (src/lib.rs) et un nombre quelconque de crates binaires (src/main.rs, src/bin/*.rs). Le code du cours lui-même est une bibliothèque avec des doctests, plus des examples/, que Cargo construit comme binaires supplémentaires.
Les dépendances se déclarent dans Cargo.toml, ou s’ajoutent avec cargo add :
[dependencies]serde = "1.0" # depuis crates.iogeometry = { path = "../geometry" } # une crate locale| Cargo | .NET | Maven/Gradle |
|---|---|---|
serde = "1.0" signifie >=1.0.0, <2.0.0 (caret, mises à jour compatibles SemVer) |
Version="1.0" signifie >=1.0 |
1.0 est une exigence souple que la résolution de conflits peut outrepasser ; les intervalles s’écrivent [1.0,2.0) |
Cargo.lock fige les versions exactes |
packages.lock.json |
fichiers de verrouillage / dependency locking |
cargo tree |
dotnet list package --include-transitive |
mvn dependency:tree |
Features : des options à la compilation
Section intitulée « Features : des options à la compilation »Une crate peut déclarer des features qui activent du code optionnel. Les features sont des interrupteurs additifs testés avec #[cfg], plus proches des symboles #if de C# que d’une configuration à l’exécution :
[features]default = []display = []// geometry/src/shapes.rs — compilé seulement quand un dépendant active `display`#[cfg(feature = "display")]impl std::fmt::Display for Rect { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}x{} rectangle", self.width, self.height) }}Si app oublie de l’activer, Rect n’a tout simplement pas d’implémentation de Display :
error[E0277]: `Rect` doesn't implement `std::fmt::Display` --> app\src\main.rs:9:20 |9 | println!("{}", Rect::new(4.0, 2.5)); // Display exists because app enables the feature | -- ^^^^^^^^^^^^^^^^^^^ `Rect` cannot be formatted with the default formatter | | | required by this formatting parameter | = help: the trait `std::fmt::Display` is not implemented for `Rect`Workspaces
Section intitulée « Workspaces »Un workspace est une solution : plusieurs packages qui partagent un même Cargo.lock et un même dossier target/, construits et testés en une seule commande. Le l10-workspace du cours :
l10-workspace/├── Cargo.toml workspace root (no package of its own)├── Cargo.lock shared by all members├── geometry/ library crate│ ├── Cargo.toml│ └── src/│ ├── lib.rs mod shapes; pub mod units; pub use shapes::{…};│ ├── shapes.rs│ └── units.rs└── app/ binary crate that depends on geometry ├── Cargo.toml └── src/main.rs[workspace]resolver = "3"members = ["geometry", "app"]
# Partagé par chaque membre qui s'y inscrit avec `x.workspace = true`[workspace.package]version = "0.1.0"edition = "2024"publish = false
[workspace.dependencies]geometry = { path = "geometry" }[package]name = "app"version.workspace = trueedition.workspace = truepublish.workspace = true
[dependencies]geometry = { workspace = true, features = ["display"] }[workspace.dependencies] joue le rôle du Central Package Management de .NET (Directory.Packages.props) ou d’une section Maven <dependencyManagement> : les versions sont déclarées une fois, les membres se contentent de workspace = true.
mod shapes; // module privé, chargé depuis src/shapes.rspub mod units; // module public, chargé depuis src/units.rs
pub use shapes::{Circle, Rect, Shape};use geometry::units::cm_to_inches;use geometry::{Circle, Rect, Shape};
fn main() { let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { radius: 1.0 }), Box::new(Rect::new(2.0, 3.0))]; let total: f64 = shapes.iter().map(|s| s.area()).sum(); println!("total area: {total:.2}");
println!("{}", Rect::new(4.0, 2.5)); println!("10 cm = {:.2} in", cm_to_inches(10.0));}total area: 9.144x2.5 rectangle10 cm = 3.94 inLa visibilité fonctionne désormais d’une crate à l’autre. shapes est un module privé et non_negative est pub(crate), donc app ne peut utiliser ni l’un ni l’autre :
error[E0603]: function `non_negative` is private --> app\src\main.rs:10:30 |10 | let _ = geometry::units::non_negative(-1.0); | ^^^^^^^^^^^^ private function |note: the function `non_negative` is defined here --> geometry\src\units.rs:6:1 | 6 | pub(crate) fn non_negative(value: f64) -> f64 { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0603]: module `shapes` is private --> app\src\main.rs:11:23 |11 | let _ = geometry::shapes::Circle { radius: 1.0 }; | ^^^^^^ private moduleCommandes courantes, lancées depuis la racine du workspace :
| Commande | Effet |
|---|---|
cargo build |
construit tous les membres |
cargo run -p app |
exécute un seul package, comme dotnet run --project app |
cargo test --workspace |
teste tous les membres |
cargo tree |
affiche le graphe des dépendances |
app v0.1.0 (…\l10-workspace\app)└── geometry v0.1.0 (…\l10-workspace\geometry)À retenir
Section intitulée « À retenir »- Un module est une portée de confidentialité ; une crate est une unité de compilation ; un package est un
Cargo.toml; un workspace regroupe des packages. - Tout est privé par défaut ;
pub(crate)correspond àinternal,pubàpublic, et les champs d’une struct ont besoin de leur proprepub. usecrée des raccourcis,pub useréexporte, les chemins partent decrate,super,selfou d’un nom de crate.mod name;chargename.rs; les fichiers qu’aucun module ne déclare ne sont pas compilés.- Les versions Cargo sont des intervalles compatibles SemVer, figés par
Cargo.lock; les features sont des interrupteurs à la compilation. - Les workspaces partagent un fichier de verrouillage et la sortie de build, et peuvent centraliser les versions dans
[workspace.dependencies].
Exercices
Section intitulée « Exercices »- Écrivez un module
temperatureavec unepub struct Celsius(f64)qui ne peut pas contenir une valeur inférieure au zéro absolu (−273.15). En dehors du module,temperature::Celsius(-500.0)ne doit pas compiler.
Solution
mod temperature { #[derive(Debug, PartialEq)] pub struct Celsius(f64);
impl Celsius { pub const ABSOLUTE_ZERO: f64 = -273.15;
pub fn new(value: f64) -> Option<Celsius> { (value >= Self::ABSOLUTE_ZERO).then_some(Celsius(value)) }
pub fn value(&self) -> f64 { self.0 } }}
use temperature::Celsius;assert_eq!(Celsius::new(-500.0), None);assert_eq!(Celsius::new(21.5).map(|c| c.value()), Some(21.5));Le champ f64 n’a pas de pub, donc le constructeur de tuple est privé :
error[E0603]: tuple struct constructor `Celsius` is private --> e10_tuple.rs:7:26 |3 | pub struct Celsius(f64); | --- a constructor is private if any of the fields is private...7 | let t = temperature::Celsius(-500.0); | ^^^^^^^ private tuple struct constructor- Le
src/main.rsd’une crate binaire contientmod network { pub mod http { pub fn get(host: &str) -> String { … } } }. Déplacez chaque module dans son propre fichier. Quels fichiers créez-vous, et que conservemain.rs?
Solution
src/├── main.rs mod network; + fn main() { … network::http::get("example.com") … }├── network.rs pub mod http;└── network/ └── http.rs pub fn get(host: &str) -> String { format!("GET http://{host}/") }main.rs déclare network, network.rs déclare http, et http.rs contient le corps. L’exécution affiche GET http://example.com/.
- Ajoutez un troisième membre,
report, àl10-workspace: une bibliothèque avecpub fn describe(shapes: &[Box<dyn Shape>]) -> Stringqui renvoie par exemple"2 shapes, total area 9.14", et un test unitaire. Qu’est-ce qui change, et dans quelCargo.toml?
Solution
Dans le Cargo.toml racine : members = ["geometry", "app", "report"]. Puis :
[package]name = "report"version.workspace = trueedition.workspace = truepublish.workspace = true
[dependencies]geometry.workspace = trueuse geometry::Shape;
pub fn describe(shapes: &[Box<dyn Shape>]) -> String { let total: f64 = shapes.iter().map(|s| s.area()).sum(); format!("{} shapes, total area {total:.2}", shapes.len())}
#[cfg(test)]mod tests { use super::*; use geometry::{Circle, Rect};
#[test] fn describes_shapes() { let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Rect::new(2.0, 3.0)), Box::new(Circle { radius: 1.0 })]; assert_eq!(describe(&shapes), "2 shapes, total area 9.14"); }}cargo test -p report exécute le test. report n’a pas besoin de la feature display, il ne la demande donc pas.