Modèles de développement Rust
Modèles Rust idiomatiques et bonnes pratiques pour construire des applications sûres, performantes et maintenables.
Quand l'utiliser
- Écrire du nouveau code Rust
- Réviser du code Rust
- Refactorer du code Rust existant
- Concevoir la structure d'une crate et l'organisation des modules
Comment ça fonctionne
Ce skill impose les conventions Rust idiomatiques dans six domaines clés : la propriété et l'emprunt pour prévenir les courses de données à la compilation, la propagation d'erreurs avec Result/? en utilisant 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 génériques pour une abstraction à coût zéro, la concurrence sécurisée via Arc<Mutex<T>>, les canaux (channels) et async/await, ainsi que des surfaces pub minimales organisées par domaine.
Principes fondamentaux
1. Propriété et emprunt
Le système de propriété 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 propriété
fn process(data: &[u8]) -> usize {
data.len()
}
// Bien : Prendre la propriété seulement quand on doit stocker ou consommer
fn store(data: Vec<u8>) -> Record {
Record { payload: data }
}
// Mal : Cloner inutilement pour éviter le borrow checker
fn process_bad(data: &Vec<u8>) -> usize {
let cloned = data.clone(); // Inutile — il suffit d'emprunter
cloned.len()
}
Utiliser Cow pour une propriété 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("échec de connexion")]
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(())
}
Combinateurs Option plutôt que du filtrage imbriqué
// Bien : Chaîne de combinateurs
fn find_user_email(users: &[User], id: u64) -> Option<String> {
users.iter()
.find(|u| u.id == id)
.map(|u| u.email.clone())
}
// Mal : Filtrage profondément imbriqué
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 avec des é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),
}
}
Filtrage exhaustif — Pas de tout-venant 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 le traitement ici
}
// Mal : Le joker cache les nouvelles variantes
match command {
Command::Start => start_service(),
_ => {} // Ignore silencieusement Stop, Restart et les variantes futures
}
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 plusieurs contraintes
fn process<T: Display + Send + 'static>(item: T) -> String {
format!("traité : {item}")
}
Objets trait pour le dispatch dynamique
// À utiliser pour des collections hétérogènes ou des 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 pour la performance (monomorphisation)
fn fast_process<H: Handler>(handler: &H, request: &Request) -> Response {
handler.handle(request)
}
Patron Newtype pour la sécurité de type
// Bien : Des types distincts empêchent de mélanger les arguments
struct UserId(u64);
struct OrderId(u64);
fn get_order(user: UserId, order: OrderId) -> Result<Order> {
// On ne peut pas é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 des 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
// Collecter dans 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();
// Collecter des 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 mutable partagé
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("le 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("récepteur déconnecté");
});
}
drop(tx); // Fermer l'expéditeur pour que l'itérateur rx se termine
for msg in rx {
println!("{msg}");
}
Async avec Tokio
use tokio::time::Duration;
async fn fetch_with_timeout(url: &str) -> Result<String> {
let response = tokio::time::timeout(
Duration::from_secs(5),
reqwest::get(url),
)
.await
.context(\