Modèles de Développement Rust
Modèles Rust idiomatiques et meilleures pratiques pour construire des applications sûres, performantes et maintenables.
Quand l'utiliser
- Lors de l'écriture de nouveau code Rust
- Lors de la révision de code Rust
- Lors du remaniement de code Rust existant
- Lors de la conception de la structure d'une crate et de la disposition des modules
Comment ça fonctionne
Cette compétence impose les conventions Rust idiomatiques dans six domaines clés : la possession et l'emprunt pour prévenir les courses de données à la compilation, la propagation d'erreurs Result/? avec thiserror pour les bibliothèques et anyhow pour les applications, les énumérations et le filtrage par motif exhaustif pour rendre les états impossibles non représentables, les traits et les génériques pour l'abstraction à coût zéro, la concurrence sûre via Arc<Mutex<T>>, les canaux et async/await, et les surfaces pub minimales organisées par domaine.
Principes fondamentaux
1. Possession et Emprunt
Le système de possession de Rust prévient les courses de données et les bugs mémoire à la compilation.
// Bien : Passer des références quand on n'a pas besoin de possession
fn process(data: &[u8]) -> usize {
data.len()
}
// Bien : Prendre possession seulement quand on doit stocker ou consommer
fn store(data: Vec<u8>) -> Record {
Record { payload: data }
}
// Mal : Cloner inutilement pour éviter le vérificateur d'emprunt
fn process_bad(data: &Vec<u8>) -> usize {
let cloned = data.clone(); // Gaspillage — il suffit d'emprunter
cloned.len()
}
Utiliser Cow pour une Possession Flexible
use std::borrow::Cow;
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input) // Coût zéro quand aucune mutation n'est nécessaire
}
}
Gestion des Erreurs
Utiliser Result et ? — Jamais unwrap() en Production
// Bien : Propager les erreurs avec contexte
use anyhow::{Context, Result};
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("échec de lecture de la configuration depuis {path}"))?;
let config: Config = toml::from_str(&content)
.with_context(|| format!("échec d'analyse de la configuration depuis {path}"))?;
Ok(config)
}
// Mal : Panique en cas d'erreur
fn load_config_bad(path: &str) -> Config {
let content = std::fs::read_to_string(path).unwrap(); // Panique !
toml::from_str(&content).unwrap()
}
Erreurs de Bibliothèque avec thiserror, Erreurs d'Application avec anyhow
// Code de bibliothèque : erreurs structurées et typées
use thiserror::Error;
#[derive(Debug, Error)]
pub enum StorageError {
#[error("enregistrement non trouvé : {id}")]
NotFound { id: String },
#[error("connexion échouée")]
Connection(#[from] std::io::Error),
#[error("données invalides : {0}")]
InvalidData(String),
}
// Code d'application : gestion d'erreurs flexible
use anyhow::{bail, Result};
fn run() -> Result<()> {
let config = load_config("app.toml")?;
if config.workers == 0 {
bail!("le nombre de workers doit être > 0");
}
Ok(())
}
Combinators Option plutôt que Correspondance Imbriquée
// Bien : Chaîne de combinators
fn find_user_email(users: &[User], id: u64) -> Option<String> {
users.iter()
.find(|u| u.id == id)
.map(|u| u.email.clone())
}
// Mal : Correspondance profondément imbriquée
fn find_user_email_bad(users: &[User], id: u64) -> Option<String> {
match users.iter().find(|u| u.id == id) {
Some(user) => match &user.email {
email => Some(email.clone()),
},
None => None,
}
}
Énumérations et Filtrage par Motif
Modéliser les États en tant qu'Énumérations
// Bien : Les états impossibles sont non représentables
enum ConnectionState {
Disconnected,
Connecting { attempt: u32 },
Connected { session_id: String },
Failed { reason: String, retries: u32 },
}
fn handle(state: &ConnectionState) {
match state {
ConnectionState::Disconnected => connect(),
ConnectionState::Connecting { attempt } if *attempt > 3 => abort(),
ConnectionState::Connecting { .. } => wait(),
ConnectionState::Connected { session_id } => use_session(session_id),
ConnectionState::Failed { retries, .. } if *retries < 5 => retry(),
ConnectionState::Failed { reason, .. } => log_failure(reason),
}
}
Correspondance Exhaustive — Pas de Fourre-Tout pour la Logique Métier
// Bien : Gérer chaque variante explicitement
match command {
Command::Start => start_service(),
Command::Stop => stop_service(),
Command::Restart => restart_service(),
// L'ajout d'une nouvelle variante force la gestion ici
}
// Mal : Le joker cache les nouvelles variantes
match command {
Command::Start => start_service(),
_ => {} // Ignore silencieusement Stop, Restart et les futures variantes
}
Traits et Génériques
Accepter des Génériques, Retourner des Types Concrets
// Bien : Entrée générique, sortie concrète
fn read_all(reader: &mut impl Read) -> std::io::Result<Vec<u8>> {
let mut buf = Vec::new();
reader.read_to_end(&mut buf)?;
Ok(buf)
}
// Bien : Bornes de trait pour contraintes multiples
fn process<T: Display + Send + 'static>(item: T) -> String {
format!("traité : {item}")
}
Objets Trait pour le Dispatching Dynamique
// Utiliser quand on a besoin de collections hétérogènes ou de systèmes de plugins
trait Handler: Send + Sync {
fn handle(&self, request: &Request) -> Response;
}
struct Router {
handlers: Vec<Box<dyn Handler>>,
}
// Utiliser des génériques quand on a besoin de performance (monomorphisation)
fn fast_process<H: Handler>(handler: &H, request: &Request) -> Response {
handler.handle(request)
}
Patron Newtype pour la Sécurité de Type
// Bien : Les types distincts empêchent la confusion des arguments
struct UserId(u64);
struct OrderId(u64);
fn get_order(user: UserId, order: OrderId) -> Result<Order> {
// Impossible d'échanger accidentellement les IDs utilisateur et commande
todo!()
}
// Mal : Facile d'échanger les arguments
fn get_order_bad(user_id: u64, order_id: u64) -> Result<Order> {
todo!()
}
Structures et Modélisation de Données
Patron Builder pour la Construction Complexe
struct ServerConfig {
host: String,
port: u16,
max_connections: usize,
}
impl ServerConfig {
fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
ServerConfigBuilder { host: host.into(), port, max_connections: 100 }
}
}
struct ServerConfigBuilder { host: String, port: u16, max_connections: usize }
impl ServerConfigBuilder {
fn max_connections(mut self, n: usize) -> Self { self.max_connections = n; self }
fn build(self) -> ServerConfig {
ServerConfig { host: self.host, port: self.port, max_connections: self.max_connections }
}
}
// Utilisation : ServerConfig::builder("localhost", 8080).max_connections(200).build()
Itérateurs et Fermetures
Préférer les Chaînes d'Itérateurs aux Boucles Manuelles
// Bien : Déclaratif, paresseux, composable
let active_emails: Vec<String> = users.iter()
.filter(|u| u.is_active)
.map(|u| u.email.clone())
.collect();
// Mal : Accumulation impérative
let mut active_emails = Vec::new();
for user in &users {
if user.is_active {
active_emails.push(user.email.clone());
}
}
Utiliser collect() avec Annotation de Type
// Collecte en différents types
let names: Vec<_> = items.iter().map(|i| &i.name).collect();
let lookup: HashMap<_, _> = items.iter().map(|i| (i.id, i)).collect();
let combined: String = parts.iter().copied().collect();
// Collecte de Result — court-circuite à la première erreur
let parsed: Result<Vec<i32>, _> = strings.iter().map(|s| s.parse()).collect();
Concurrence
Arc<Mutex<T>> pour l'État Partagé Mutable
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..10).map(|_| {
let counter = Arc::clone(&counter);
std::thread::spawn(move || {
let mut num = counter.lock().expect("mutex empoisonné");
*num += 1;
})
}).collect();
for handle in handles {
handle.join().expect("thread worker a paniqué");
}
Canaux pour le Passage de Messages
use std::sync::mpsc;
let (tx, rx) = mpsc::sync_channel(16); // Canal borné avec contre-pression
for i in 0..5 {
let tx = tx.clone();
std::thread::spawn(move || {
tx.send(format!("message {i}")).expect(\