# Questions d'entretien Rust : Guide complet 2026 > Les 25 questions d'entretien Rust les plus fréquentes. Ownership, borrowing, lifetimes, traits, async et concurrence avec réponses détaillées et exemples de code. - Published: 2026-01-26 - Updated: 2026-03-28 - Author: SharpSkill - Tags: rust, interview, systems programming, ownership, entretien technique - Reading time: 22 min --- Les entretiens Rust évaluent la compréhension du système de propriété unique au langage, la gestion mémoire sans garbage collector, et la capacité à écrire du code concurrent sûr. Ce guide couvre les questions essentielles, des fondamentaux de l'ownership jusqu'aux patterns avancés d'async et de concurrence. > **Conseil pour l'entretien** > > Les recruteurs valorisent les explications qui démontrent une compréhension des garanties de sécurité mémoire de Rust. Expliquer comment le compilateur prévient les bugs à la compilation fait la différence. ## Ownership et Borrowing ### Question 1 : Expliquez le système d'ownership en Rust L'ownership est le concept central de Rust qui permet la gestion mémoire sans garbage collector tout en garantissant la sécurité mémoire à la compilation. ```rust // ownership_basics.rs // Les trois règles fondamentales de l'ownership fn main() { // Règle 1 : Chaque valeur a un unique propriétaire let s1 = String::from("hello"); // s1 est propriétaire // Règle 2 : Une seule variable peut être propriétaire à la fois let s2 = s1; // s1 est MOVED vers s2 // println!("{}", s1); // ERREUR : s1 n'est plus valide println!("{}", s2); // OK : s2 est maintenant propriétaire // Règle 3 : Quand le propriétaire sort du scope, la valeur est drop { let s3 = String::from("world"); // s3 est valide ici } // s3 sort du scope, la mémoire est libérée automatiquement // Types Copy : les types simples sont copiés, pas déplacés let x = 5; let y = x; // x est COPIÉ, pas déplacé println!("x = {}, y = {}", x, y); // Les deux sont valides } // Le move en action avec les fonctions fn take_ownership(s: String) { // s prend possession de la String println!("{}", s); } // s est drop ici, mémoire libérée fn makes_copy(i: i32) { // i est une copie de l'argument println!("{}", i); } // i sort du scope, rien de spécial (type Copy) fn ownership_with_functions() { let s = String::from("hello"); take_ownership(s); // s est moved dans la fonction // println!("{}", s); // ERREUR : s n'est plus valide let x = 5; makes_copy(x); // x est copié println!("{}", x); // OK : x est toujours valide } ``` L'ownership élimine les bugs de mémoire courants : use-after-free, double-free, et fuites mémoire. Le compilateur garantit ces propriétés à la compilation. ### Question 2 : Quelle est la différence entre borrowing immutable et mutable ? Le borrowing permet d'utiliser une valeur sans en prendre possession, avec des règles strictes pour prévenir les data races. ```rust // borrowing_rules.rs // Références immutables et mutables fn main() { let mut s = String::from("hello"); // RÉFÉRENCES IMMUTABLES (&T) // Peuvent coexister en nombre illimité let r1 = &s; // référence immutable let r2 = &s; // autre référence immutable println!("{} and {}", r1, r2); // OK // RÉFÉRENCE MUTABLE (&mut T) // Une seule à la fois, et pas de références immutables simultanées let r3 = &mut s; // référence mutable // let r4 = &s; // ERREUR : ne peut pas avoir immutable et mutable ensemble // let r5 = &mut s; // ERREUR : une seule référence mutable r3.push_str(" world"); println!("{}", r3); // Les scopes de référence sont limités à leur dernière utilisation let r6 = &s; // OK car r3 n'est plus utilisé println!("{}", r6); } // Exemple pratique : modifier une structure struct User { name: String, age: u32, } impl User { // &self : accès en lecture seule fn get_name(&self) -> &str { &self.name } // &mut self : accès en modification fn set_name(&mut self, name: String) { self.name = name; } // self : prend possession (consomme l'instance) fn into_name(self) -> String { self.name // L'instance User n'existe plus après } } fn borrowing_with_structs() { let mut user = User { name: String::from("Alice"), age: 30, }; // Lecture println!("Name: {}", user.get_name()); // Modification user.set_name(String::from("Bob")); // Consommation let name = user.into_name(); // user.age; // ERREUR : user a été consommé } ``` Ces règles garantissent l'absence de data races à la compilation. Aucun autre langage n'offre cette garantie sans sacrifice de performance. > **Non-Lexical Lifetimes** > > Depuis Rust 2018, le compilateur utilise les NLL (Non-Lexical Lifetimes) pour déterminer plus précisément quand une référence n'est plus utilisée, permettant plus de flexibilité. ### Question 3 : Que sont les lifetimes et quand les annoter ? Les lifetimes sont des annotations qui indiquent au compilateur la durée de validité des références, permettant de prévenir les références pendantes. ```rust // lifetimes.rs // Comprendre et annoter les durées de vie // ERREUR : référence pendante (dangling reference) // fn dangling() -> &String { // let s = String::from("hello"); // &s // s est drop à la fin de la fonction, référence invalide // } // Le compilateur infère souvent les lifetimes automatiquement fn first_word(s: &str) -> &str { // Lifetime élidée : le compilateur comprend que le retour // a la même lifetime que l'entrée match s.find(' ') { Some(i) => &s[..i], None => s, } } // Annotation explicite nécessaire avec plusieurs références fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // 'a signifie : le retour vit au moins aussi longtemps // que la plus courte des deux entrées if x.len() > y.len() { x } else { y } } fn lifetime_example() { let string1 = String::from("long string"); let result; { let string2 = String::from("xyz"); result = longest(&string1, &string2); println!("Longest: {}", result); // OK ici } // println!("{}", result); // ERREUR : string2 est drop } // Lifetimes dans les structures struct ImportantExcerpt<'a> { part: &'a str, // La struct ne peut pas vivre plus longtemps que part } impl<'a> ImportantExcerpt<'a> { // Méthode qui retourne une référence avec la même lifetime fn level(&self) -> i32 { 3 } // Lifetime élidée pour &self retournant une nouvelle référence fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention: {}", announcement); self.part // Retourne avec lifetime 'a } } // Static lifetime : vit pour toute la durée du programme fn static_lifetime() { let s: &'static str = "hello"; // Stocké dans le binaire // Les constantes ont une lifetime 'static implicite const MAX_POINTS: u32 = 100_000; } // Combinaison de lifetimes et génériques fn longest_with_announcement<'a, T>( x: &'a str, y: &'a str, ann: T, ) -> &'a str where T: std::fmt::Display, { println!("Announcement: {}", ann); if x.len() > y.len() { x } else { y } } ``` Les lifetimes sont vérifiées à la compilation. Si le code compile, les références sont garanties valides. ## Traits et Génériques ### Question 4 : Comment fonctionnent les traits en Rust ? Les traits définissent un comportement partagé entre différents types, similaires aux interfaces mais avec des fonctionnalités supplémentaires. ```rust // traits_basics.rs // Définition et implémentation des traits // Définition d'un trait trait Summary { // Méthode requise (sans corps) fn summarize(&self) -> String; // Méthode avec implémentation par défaut fn summarize_author(&self) -> String { String::from("(Anonymous)") } // Méthode par défaut qui appelle une méthode requise fn full_summary(&self) -> String { format!("By {} - {}", self.summarize_author(), self.summarize()) } } // Implémentation pour différents types struct NewsArticle { headline: String, location: String, author: String, content: String, } impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) } fn summarize_author(&self) -> String { format!("@{}", self.author) } } struct Tweet { username: String, content: String, reply: bool, retweet: bool, } impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) } } // Trait bounds : restreindre les génériques fn notify(item: &T) { println!("Breaking news! {}", item.summarize()); } // Syntaxe alternative avec where fn notify_verbose(item: &T) where T: Summary, { println!("Breaking news! {}", item.summarize()); } // Multiples trait bounds fn notify_complex(item: &T) { println!("{}", item); } // Retourner un type qui implémente un trait fn create_summarizable() -> impl Summary { Tweet { username: String::from("rust_lang"), content: String::from("Rust 2026 is amazing!"), reply: false, retweet: false, } } ``` Les traits permettent le polymorphisme sans héritage de classe, favorisant la composition sur l'héritage. ### Question 5 : Expliquez la différence entre généricité statique et dynamique Rust offre deux approches pour le polymorphisme : la monomorphisation (statique) et les trait objects (dynamique). ```rust // static_vs_dynamic_dispatch.rs // Dispatch statique vs dynamique trait Animal { fn speak(&self) -> String; fn name(&self) -> &str; } struct Dog { name: String } struct Cat { name: String } impl Animal for Dog { fn speak(&self) -> String { String::from("Woof!") } fn name(&self) -> &str { &self.name } } impl Animal for Cat { fn speak(&self) -> String { String::from("Meow!") } fn name(&self) -> &str { &self.name } } // DISPATCH STATIQUE (monomorphisation) // Le compilateur génère une version pour chaque type concret fn make_speak_static(animal: &T) { // À la compilation, devient make_speak_Dog et make_speak_Cat println!("{} says {}", animal.name(), animal.speak()); } // Avantages : inlining possible, aucun overhead à l'exécution // Inconvénients : code binaire plus gros, type connu à la compilation // DISPATCH DYNAMIQUE (trait objects) // Utilise une vtable pour résoudre les méthodes à l'exécution fn make_speak_dynamic(animal: &dyn Animal) { // Résolu via une table de pointeurs (vtable) à l'exécution println!("{} says {}", animal.name(), animal.speak()); } // Avantages : peut stocker différents types, binaire plus petit // Inconvénients : overhead de l'indirection, pas d'inlining fn main() { let dog = Dog { name: String::from("Rex") }; let cat = Cat { name: String::from("Whiskers") }; // Statique : le type est connu à la compilation make_speak_static(&dog); make_speak_static(&cat); // Dynamique : le type est résolu à l'exécution make_speak_dynamic(&dog); make_speak_dynamic(&cat); // Collection hétérogène (nécessite le dispatch dynamique) let animals: Vec> = vec![ Box::new(Dog { name: String::from("Buddy") }), Box::new(Cat { name: String::from("Luna") }), ]; for animal in animals.iter() { println!("{} says {}", animal.name(), animal.speak()); } } // Object safety : pas tous les traits peuvent devenir des trait objects trait ObjectSafe { fn method(&self); // Pas de Self dans le retour // Pas de paramètres génériques } // NOT object safe (ne peut pas être dyn NotObjectSafe) trait NotObjectSafe { fn create() -> Self; // Self dans le retour fn generic(&self, t: T); // Générique } ``` Le dispatch statique est préférable pour la performance. Le dispatch dynamique est utile pour les collections hétérogènes et la flexibilité. ## Gestion des Erreurs ### Question 6 : Comment gérer les erreurs avec Result et Option ? Rust n'a pas d'exceptions. La gestion d'erreurs se fait via les types `Result` et `Option` avec le pattern matching. ```rust // error_handling.rs // Gestion idiomatique des erreurs en Rust use std::fs::File; use std::io::{self, Read}; // Option : présence ou absence de valeur fn find_user(id: u32) -> Option { match id { 1 => Some(String::from("Alice")), 2 => Some(String::from("Bob")), _ => None, // Pas d'utilisateur trouvé } } // Result : succès ou erreur fn divide(a: f64, b: f64) -> Result { if b == 0.0 { Err(String::from("Division by zero")) } else { Ok(a / b) } } fn option_combinators() { let user = find_user(1); // Pattern matching match user { Some(name) => println!("Found: {}", name), None => println!("Not found"), } // unwrap_or : valeur par défaut let name = find_user(99).unwrap_or(String::from("Unknown")); // map : transformer la valeur si présente let upper = find_user(1).map(|n| n.to_uppercase()); // and_then (flatMap) : chaîner des Options let first_char = find_user(1).and_then(|n| n.chars().next()); // if let : pattern matching simplifié if let Some(name) = find_user(2) { println!("User 2 is {}", name); } } fn result_handling() -> Result<(), Box> { // L'opérateur ? propage l'erreur automatiquement let result = divide(10.0, 2.0)?; println!("Result: {}", result); // Équivalent à : // let result = match divide(10.0, 2.0) { // Ok(v) => v, // Err(e) => return Err(e.into()), // }; Ok(()) } // Lecture de fichier avec propagation d'erreurs fn read_file_contents(path: &str) -> Result { let mut file = File::open(path)?; // Propage l'erreur si échec let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) } // Erreurs personnalisées #[derive(Debug)] enum AppError { IoError(io::Error), ParseError(String), NotFound(String), } impl std::fmt::Display for AppError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { AppError::IoError(e) => write!(f, "IO error: {}", e), AppError::ParseError(s) => write!(f, "Parse error: {}", s), AppError::NotFound(s) => write!(f, "Not found: {}", s), } } } impl std::error::Error for AppError {} // Conversion automatique avec From impl From for AppError { fn from(error: io::Error) -> Self { AppError::IoError(error) } } fn complex_operation() -> Result { let contents = std::fs::read_to_string("config.txt")?; // Auto-convert if contents.is_empty() { return Err(AppError::NotFound(String::from("Config is empty"))); } Ok(contents) } ``` L'opérateur `?` rend le code concis tout en forçant la gestion explicite des erreurs. Pas de surprises à l'exécution. > **Éviter unwrap en production** > > `unwrap()` et `expect()` paniquent si la valeur est None ou Err. Les réserver au prototypage ou aux cas où l'échec est impossible. En production, préférer la propagation avec `?` ou les combinators. ### Question 7 : Comment créer des erreurs personnalisées avec thiserror ? La crate `thiserror` simplifie la création d'erreurs personnalisées ergonomiques. ```rust // custom_errors.rs // Erreurs personnalisées avec thiserror use thiserror::Error; // Définition d'erreurs avec derive macro #[derive(Error, Debug)] pub enum DataStoreError { #[error("connection failed: {0}")] ConnectionFailed(String), #[error("query failed: {query}")] QueryFailed { query: String, source: std::io::Error }, #[error("record not found: id={id}")] NotFound { id: u64 }, #[error("invalid data: {0}")] InvalidData(#[from] serde_json::Error), #[error(transparent)] // Délègue Display à la source Other(#[from] anyhow::Error), } // Implémentation avec contexte riche pub struct DataStore { connection_string: String, } impl DataStore { pub fn connect(conn_str: &str) -> Result { if conn_str.is_empty() { return Err(DataStoreError::ConnectionFailed( "Empty connection string".into() )); } Ok(Self { connection_string: conn_str.to_string() }) } pub fn get_record(&self, id: u64) -> Result { // Simulation d'une requête if id == 0 { return Err(DataStoreError::NotFound { id }); } Ok(Record { id, data: format!("Record {}", id) }) } } pub struct Record { pub id: u64, pub data: String, } // Utilisation avec anyhow pour les applications use anyhow::{Context, Result}; fn application_code() -> Result<()> { let store = DataStore::connect("postgres://localhost/db") .context("Failed to connect to database")?; let record = store.get_record(42) .context("Failed to fetch user record")?; println!("Got: {}", record.data); Ok(()) } // Pattern : convertir des erreurs avec contexte fn read_config() -> Result { let contents = std::fs::read_to_string("config.toml") .context("Failed to read config file")?; let config: Config = toml::from_str(&contents) .context("Failed to parse config file")?; Ok(config) } #[derive(Debug)] struct Config { // ... } ``` `thiserror` est idéal pour les bibliothèques (erreurs typées), tandis que `anyhow` convient aux applications (flexibilité maximale). ## Smart Pointers ### Question 8 : Expliquez Box, Rc, Arc et RefCell Les smart pointers gèrent la mémoire heap et permettent des patterns que l'ownership simple ne permet pas. ```rust // smart_pointers.rs // Les principaux smart pointers de Rust use std::rc::Rc; use std::sync::Arc; use std::cell::RefCell; // BOX : allocation sur le heap // Utilisé pour : types récursifs, grands types, trait objects fn box_example() { // Allocation heap simple let b = Box::new(5); println!("b = {}", b); // Type récursif (impossible sans Box) #[derive(Debug)] enum List { Cons(i32, Box), Nil, } let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil)))))); println!("{:?}", list); } // RC : Reference Counting (single-threaded) // Plusieurs propriétaires pour la même donnée fn rc_example() { let data = Rc::new(vec![1, 2, 3]); // Clone incrémente le compteur de références let data_clone1 = Rc::clone(&data); // count = 2 let data_clone2 = Rc::clone(&data); // count = 3 println!("Reference count: {}", Rc::strong_count(&data)); // 3 // Chaque clone peut lire les données println!("data_clone1: {:?}", data_clone1); // Les données sont libérées quand le dernier Rc est drop } // ARC : Atomic Reference Counting (thread-safe) // Comme Rc mais utilisable entre threads fn arc_example() { use std::thread; let data = Arc::new(vec![1, 2, 3, 4, 5]); let mut handles = vec![]; for i in 0..3 { let data_clone = Arc::clone(&data); let handle = thread::spawn(move || { // Chaque thread a son propre Arc println!("Thread {}: {:?}", i, data_clone); }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } } // REFCELL : Interior Mutability // Permet la mutation même avec une référence immutable fn refcell_example() { let data = RefCell::new(5); // borrow() retourne une référence immutable println!("Value: {}", *data.borrow()); // borrow_mut() retourne une référence mutable *data.borrow_mut() += 1; println!("After mutation: {}", *data.borrow()); // Les règles de borrowing sont vérifiées à l'EXÉCUTION // Panique si on viole les règles // let r1 = data.borrow(); // let r2 = data.borrow_mut(); // PANIQUE : déjà emprunté } // Combinaison courante : Rc> // Plusieurs propriétaires avec mutation possible fn rc_refcell_example() { #[derive(Debug)] struct Node { value: i32, children: Vec>>, } let node1 = Rc::new(RefCell::new(Node { value: 1, children: vec![], })); let node2 = Rc::new(RefCell::new(Node { value: 2, children: vec![Rc::clone(&node1)], // node1 est child de node2 })); // Modifier node1 depuis n'importe où node1.borrow_mut().value = 10; println!("node2 child value: {}", node2.borrow().children[0].borrow().value); // 10 } // Pour les threads : Arc> ou Arc> fn arc_mutex_example() { use std::sync::Mutex; use std::thread; let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Final count: {}", *counter.lock().unwrap()); // 10 } ``` Choisir le bon smart pointer selon le contexte : `Box` pour l'heap simple, `Rc`/`Arc` pour le partage, `RefCell`/`Mutex` pour la mutabilité intérieure. ## Concurrence ### Question 9 : Comment Rust garantit-il la sécurité des threads ? Le système de types de Rust prévient les data races à la compilation via les traits `Send` et `Sync`. ```rust // thread_safety.rs // Garanties de sécurité concurrente use std::thread; use std::sync::{Arc, Mutex, mpsc}; // SEND : un type peut être transféré vers un autre thread // SYNC : un type peut être partagé entre threads via références // La plupart des types sont Send et Sync automatiquement // Exceptions : Rc (pas Send/Sync), RefCell (pas Sync), raw pointers fn send_example() { let data = vec![1, 2, 3]; // Vec est Send, donc on peut le déplacer vers un autre thread let handle = thread::spawn(move || { println!("Data in thread: {:?}", data); }); handle.join().unwrap(); } // Le compilateur empêche les erreurs de concurrence fn compile_time_safety() { // Ceci ne compilerait PAS : // let data = std::rc::Rc::new(5); // thread::spawn(move || { // println!("{}", data); // ERREUR : Rc n'est pas Send // }); // Solution : utiliser Arc let data = Arc::new(5); let data_clone = Arc::clone(&data); thread::spawn(move || { println!("{}", data_clone); // OK : Arc est Send }); } // Mutex pour la mutation partagée thread-safe fn mutex_pattern() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { // lock() bloque jusqu'à obtenir l'accès exclusif let mut num = counter.lock().unwrap(); *num += 1; // Le MutexGuard est drop ici, libérant le lock }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); } // RwLock pour lectures multiples / écriture exclusive fn rwlock_example() { use std::sync::RwLock; let data = Arc::new(RwLock::new(vec![1, 2, 3])); let mut handles = vec![]; // Plusieurs lecteurs simultanés for i in 0..3 { let data = Arc::clone(&data); handles.push(thread::spawn(move || { let read = data.read().unwrap(); println!("Reader {}: {:?}", i, *read); })); } // Un seul écrivain à la fois { let data = Arc::clone(&data); handles.push(thread::spawn(move || { let mut write = data.write().unwrap(); write.push(4); println!("Writer added 4"); })); } for handle in handles { handle.join().unwrap(); } } // Channels pour la communication entre threads fn channel_example() { let (tx, rx) = mpsc::channel(); // Multi-producer, single-consumer // Cloner le sender pour plusieurs producteurs let tx1 = tx.clone(); thread::spawn(move || { tx1.send("from thread 1").unwrap(); }); thread::spawn(move || { tx.send("from thread 2").unwrap(); }); // Recevoir les messages for received in rx { println!("Got: {}", received); } } ``` "Fearless concurrency" : si le code compile, il n'y a pas de data races. Le compilateur est le premier rempart. > **Poison des Mutex** > > Si un thread panique pendant qu'il détient un Mutex, le Mutex est "empoisonné". Les appels suivants à `lock()` retournent une erreur qui peut être récupérée avec `into_inner()`. ### Question 10 : Comment fonctionne async/await en Rust ? L'async en Rust est basé sur des Futures à coût nul (zero-cost), sans runtime intégré au langage. ```rust // async_await.rs // Programmation asynchrone en Rust use tokio::time::{sleep, Duration}; // async fn retourne un Future qui doit être exécuté async fn fetch_data(url: &str) -> Result { // await suspend l'exécution sans bloquer le thread let response = reqwest::get(url).await?; let body = response.text().await?; Ok(body) } // Les Futures sont lazy : rien ne s'exécute sans await ou poll async fn lazy_example() { let future = async { println!("This won't print yet"); }; // Rien ne s'est passé future.await; // Maintenant ça s'exécute } // Exécution parallèle de futures async fn parallel_execution() { // join! exécute plusieurs futures en parallèle let (result1, result2) = tokio::join!( fetch_data("https://api.example.com/1"), fetch_data("https://api.example.com/2"), ); println!("Results: {:?}, {:?}", result1, result2); } // select! pour la première future complétée async fn race_example() { tokio::select! { result = fetch_data("https://api1.example.com") => { println!("API 1 responded first: {:?}", result); } result = fetch_data("https://api2.example.com") => { println!("API 2 responded first: {:?}", result); } _ = sleep(Duration::from_secs(5)) => { println!("Timeout!"); } } } // Streams : itérateurs asynchrones use tokio_stream::StreamExt; async fn stream_example() { let mut stream = tokio_stream::iter(vec![1, 2, 3, 4, 5]); while let Some(value) = stream.next().await { println!("Got: {}", value); } } // Spawn pour les tâches en arrière-plan async fn spawn_tasks() { let handle = tokio::spawn(async { sleep(Duration::from_secs(1)).await; "Task completed" }); println!("Task spawned, doing other work..."); let result = handle.await.unwrap(); println!("Result: {}", result); } // Point d'entrée avec tokio #[tokio::main] async fn main() { // Le runtime tokio exécute les futures parallel_execution().await; } // Alternative : runtime multi-threaded ou single-threaded #[tokio::main(flavor = "current_thread")] async fn main_single_thread() { // Tout s'exécute sur un seul thread } #[tokio::main(flavor = "multi_thread", worker_threads = 4)] async fn main_multi_thread() { // Pool de 4 threads workers } ``` Rust async est "bring your own runtime" : tokio, async-std, ou smol. Cette flexibilité permet des optimisations spécifiques au cas d'usage. ## Patterns Avancés ### Question 11 : Expliquez le pattern Builder en Rust Le pattern Builder est idiomatique en Rust pour construire des structures complexes avec de nombreux champs optionnels. ```rust // builder_pattern.rs // Pattern Builder idiomatique en Rust #[derive(Debug, Clone)] pub struct Server { host: String, port: u16, max_connections: usize, timeout_seconds: u64, tls_enabled: bool, tls_cert_path: Option, } // Builder avec l'approche consommante (ownership) #[derive(Default)] pub struct ServerBuilder { host: String, port: u16, max_connections: usize, timeout_seconds: u64, tls_enabled: bool, tls_cert_path: Option, } impl ServerBuilder { pub fn new() -> Self { Self { host: String::from("localhost"), port: 8080, max_connections: 100, timeout_seconds: 30, tls_enabled: false, tls_cert_path: None, } } // Chaque méthode prend self et retourne Self pour le chaînage pub fn host(mut self, host: impl Into) -> Self { self.host = host.into(); self } pub fn port(mut self, port: u16) -> Self { self.port = port; self } pub fn max_connections(mut self, max: usize) -> Self { self.max_connections = max; self } pub fn timeout(mut self, seconds: u64) -> Self { self.timeout_seconds = seconds; self } pub fn enable_tls(mut self, cert_path: impl Into) -> Self { self.tls_enabled = true; self.tls_cert_path = Some(cert_path.into()); self } // build() consomme le builder et crée la structure finale pub fn build(self) -> Result { if self.tls_enabled && self.tls_cert_path.is_none() { return Err("TLS enabled but no certificate path provided".into()); } Ok(Server { host: self.host, port: self.port, max_connections: self.max_connections, timeout_seconds: self.timeout_seconds, tls_enabled: self.tls_enabled, tls_cert_path: self.tls_cert_path, }) } } // Utilisation fluide fn create_server() -> Result { ServerBuilder::new() .host("0.0.0.0") .port(443) .max_connections(1000) .timeout(60) .enable_tls("/etc/ssl/cert.pem") .build() } // Alternative avec derive macro (typed-builder crate) // #[derive(TypedBuilder)] // pub struct Config { // #[builder(default = "localhost".to_string())] // host: String, // #[builder(default = 8080)] // port: u16, // } // Pattern avec validation par types (typestate pattern) pub struct Unvalidated; pub struct Validated; pub struct Request { url: String, method: String, headers: Vec<(String, String)>, _state: std::marker::PhantomData, } impl Request { pub fn new(url: &str) -> Self { Self { url: url.to_string(), method: "GET".to_string(), headers: vec![], _state: std::marker::PhantomData, } } pub fn method(mut self, method: &str) -> Self { self.method = method.to_string(); self } // validate() change le type d'état pub fn validate(self) -> Result, String> { if self.url.is_empty() { return Err("URL cannot be empty".into()); } Ok(Request { url: self.url, method: self.method, headers: self.headers, _state: std::marker::PhantomData, }) } } impl Request { // send() n'est disponible que sur les requêtes validées pub async fn send(self) -> Result { // Implémentation... todo!() } } struct Response; ``` Le typestate pattern garantit à la compilation que certaines opérations ne peuvent être appelées que dans le bon état. ### Question 12 : Comment implémenter un trait pour des types externes ? Le "orphan rule" empêche d'implémenter un trait externe pour un type externe, mais il existe des solutions. ```rust // newtype_pattern.rs // Le pattern Newtype pour contourner l'orphan rule use std::fmt; // RÈGLE ORPHAN : on ne peut pas implémenter Display (std) pour Vec (std) // impl fmt::Display for Vec { ... } // ERREUR // SOLUTION 1 : Newtype wrapper struct Wrapper(Vec); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } } fn newtype_example() { let w = Wrapper(vec![ String::from("hello"), String::from("world"), ]); println!("{}", w); // [hello, world] } // Accès transparent avec Deref use std::ops::Deref; impl Deref for Wrapper { type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 } } fn deref_example() { let w = Wrapper(vec![String::from("test")]); println!("Length: {}", w.len()); // Appelle Vec::len via Deref } // SOLUTION 2 : Extension trait (pour ajouter des méthodes) trait VecExt { fn first_or_default(&self) -> Option<&T>; } impl VecExt for Vec { fn first_or_default(&self) -> Option<&T> { self.first() } } fn extension_trait_example() { let v = vec![1, 2, 3]; println!("First: {:?}", v.first_or_default()); } // Newtype avec sémantique de domaine #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct Email(String); impl Email { pub fn new(email: &str) -> Result { if email.contains('@') && email.contains('.') { Ok(Self(email.to_string())) } else { Err("Invalid email format") } } pub fn as_str(&self) -> &str { &self.0 } } impl fmt::Display for Email { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] struct UserId(u64); impl UserId { pub fn new(id: u64) -> Self { Self(id) } } // Les newtypes ajoutent la sécurité de types sans overhead fn process_user(id: UserId, email: Email) { println!("Processing user {} with email {}", id.0, email); } fn type_safety_example() { let id = UserId::new(42); let email = Email::new("user@example.com").unwrap(); process_user(id, email); // Ceci ne compilerait pas : // process_user(email, id); // Types inversés // process_user(UserId::new(42), "string"); // String au lieu d'Email } ``` Les newtypes ont un coût nul à l'exécution grâce à la garantie de représentation mémoire identique. ### Question 13 : Comment utiliser les macros procédurales ? Les macros procédurales permettent de générer du code à la compilation, comme les derives personnalisés. ```rust // procedural_macros.rs // Comprendre les macros procédurales // Les macros proc se définissent dans une crate séparée avec proc-macro = true // Crate: my_derive (Cargo.toml: proc-macro = true) use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; // DERIVE MACRO : #[derive(MyTrait)] #[proc_macro_derive(MyDebug)] pub fn my_debug_derive(input: TokenStream) -> TokenStream { // Parse l'entrée comme une définition de type let input = parse_macro_input!(input as DeriveInput); let name = input.ident; // Génère le code d'implémentation let expanded = quote! { impl std::fmt::Debug for #name { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, stringify!(#name)) } } }; TokenStream::from(expanded) } // ATTRIBUTE MACRO : #[my_attribute] #[proc_macro_attribute] pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { // attr contient les arguments de l'attribut // item contient l'élément annoté (fonction, struct, etc.) let method_path = attr.to_string(); // "GET, /users" let input = parse_macro_input!(item as syn::ItemFn); let fn_name = &input.sig.ident; let expanded = quote! { #input // Code généré en plus fn register_#fn_name() { println!("Registered route: {}", #method_path); } }; TokenStream::from(expanded) } // FUNCTION-LIKE MACRO : my_macro!(...) #[proc_macro] pub fn make_answer(_input: TokenStream) -> TokenStream { "fn answer() -> u32 { 42 }".parse().unwrap() } // --- Utilisation dans le code client --- // Derive macro #[derive(MyDebug)] struct Point { x: i32, y: i32, } // Attribute macro #[route("GET", "/users")] fn get_users() -> Vec { vec![] } // Function-like macro make_answer!(); // Génère fn answer() -> u32 { 42 } fn main() { let p = Point { x: 1, y: 2 }; println!("{:?}", p); // Utilise notre MyDebug println!("Answer: {}", answer()); // 42 } struct User; ``` Les macros procédurales sont puissantes pour le code répétitif : sérialisation, routing web, validation, etc. ## Memory Safety et Unsafe ### Question 14 : Quand et comment utiliser unsafe ? Le bloc `unsafe` permet de contourner certaines vérifications du compilateur pour du code bas niveau. ```rust // unsafe_rust.rs // Comprendre unsafe et ses garanties // Les 5 superpouvoirs de unsafe : // 1. Déréférencer des raw pointers // 2. Appeler des fonctions unsafe // 3. Accéder/modifier des variables statiques mutables // 4. Implémenter des traits unsafe // 5. Accéder aux champs d'unions // RAW POINTERS fn raw_pointers() { let mut num = 5; // Créer des raw pointers est safe let r1 = &num as *const i32; let r2 = &mut num as *mut i32; // Déréférencer nécessite unsafe unsafe { println!("r1 is: {}", *r1); *r2 = 10; println!("r2 is: {}", *r2); } } // FONCTION UNSAFE // La fonction garantit la sécurité SI les préconditions sont respectées unsafe fn dangerous() { // Code qui suppose que l'appelant a vérifié les invariants } fn call_dangerous() { // Doit être dans un bloc unsafe unsafe { dangerous(); } } // ABSTRACTION SAFE sur du code unsafe fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = values.len(); let ptr = values.as_mut_ptr(); assert!(mid <= len); // Vérification à l'exécution unsafe { // On sait que les deux slices ne se chevauchent pas ( std::slice::from_raw_parts_mut(ptr, mid), std::slice::from_raw_parts_mut(ptr.add(mid), len - mid), ) } } // FFI : appeler du code C extern "C" { fn abs(input: i32) -> i32; } fn call_c_function() { unsafe { println!("Absolute value: {}", abs(-3)); } } // Exporter une fonction pour C #[no_mangle] pub extern "C" fn call_from_c() { println!("Called from C!"); } // STATIC MUTABLE static mut COUNTER: u32 = 0; fn increment_counter() { unsafe { COUNTER += 1; println!("COUNTER: {}", COUNTER); } } // TRAIT UNSAFE unsafe trait Dangerous { // Les implémenteurs garantissent des invariants } unsafe impl Dangerous for i32 { // L'implémenteur affirme respecter les invariants du trait } // Exemple pratique : structure avec pointeur interne pub struct MyVec { ptr: *mut T, len: usize, capacity: usize, } impl MyVec { pub fn new() -> Self { Self { ptr: std::ptr::null_mut(), len: 0, capacity: 0, } } pub fn push(&mut self, value: T) { if self.len == self.capacity { self.grow(); } unsafe { std::ptr::write(self.ptr.add(self.len), value); } self.len += 1; } fn grow(&mut self) { // Allocation/réallocation unsafe... } } impl Drop for MyVec { fn drop(&mut self) { unsafe { // Libérer la mémoire correctement for i in 0..self.len { std::ptr::drop_in_place(self.ptr.add(i)); } if self.capacity > 0 { let layout = std::alloc::Layout::array::(self.capacity).unwrap(); std::alloc::dealloc(self.ptr as *mut u8, layout); } } } } ``` > **Règle d'or de unsafe** > > Minimiser la surface de code unsafe. Encapsuler le code unsafe dans des abstractions safe qui garantissent les invariants. Le code unsafe ne doit jamais pouvoir corrompre la mémoire safe environnante. ### Question 15 : Comment fonctionne le borrow checker ? Le borrow checker est le cœur du compilateur Rust qui vérifie les règles de propriété et d'emprunt. ```rust // borrow_checker.rs // Comprendre le fonctionnement du borrow checker fn borrow_checker_basics() { let mut v = vec![1, 2, 3]; // RÈGLE 1 : Soit plusieurs références immutables, soit une mutable let r1 = &v; let r2 = &v; println!("{:?} {:?}", r1, r2); // OK : multiples références immutables // À partir d'ici, r1 et r2 ne sont plus utilisés (NLL) let r3 = &mut v; // OK grâce aux Non-Lexical Lifetimes r3.push(4); } // Le borrow checker trace les lifetimes fn lifetime_tracking() { let mut data = String::from("hello"); let slice = &data[..]; // Emprunt immutable commence // data.push_str(" world"); // ERREUR : ne peut pas muter pendant un emprunt println!("{}", slice); // Dernière utilisation de slice data.push_str(" world"); // OK : emprunt terminé } // Problèmes courants et solutions mod common_patterns { // Problème : emprunter deux champs mutables struct Data { field1: Vec, field2: Vec, } fn problem(data: &mut Data) { // Ceci ne compile pas directement parfois : // let f1 = &mut data.field1; // let f2 = &mut data.field2; // Solution : le destructuring let Data { field1, field2 } = data; field1.push(1); field2.push(2); } // Problème : itérer et modifier fn iterate_and_modify() { let mut v = vec![1, 2, 3, 4, 5]; // Ne compile pas : // for &x in &v { // if x % 2 == 0 { // v.push(x * 2); // ERREUR : emprunté par l'itérateur // } // } // Solution 1 : collecter les indices d'abord let to_add: Vec = v.iter() .filter(|&&x| x % 2 == 0) .map(|&x| x * 2) .collect(); v.extend(to_add); // Solution 2 : utiliser indices explicites let len = v.len(); for i in 0..len { if v[i] % 2 == 0 { let new_val = v[i] * 2; v.push(new_val); } } } // Problème : self-referential struct // struct SelfRef { // data: String, // slice: &str, // Référence vers data - IMPOSSIBLE // } // Solution : utiliser des indices ou des crates comme ouroboros struct SafeSelfRef { data: String, slice_start: usize, slice_end: usize, } impl SafeSelfRef { fn get_slice(&self) -> &str { &self.data[self.slice_start..self.slice_end] } } } // Pattern pour contourner les limitations mod workarounds { use std::cell::RefCell; // Interior mutability quand le borrow checker est trop restrictif struct Graph { nodes: RefCell>, } struct Node { value: i32, } impl Graph { fn add_node(&self, value: i32) { // Mutation possible malgré &self self.nodes.borrow_mut().push(Node { value }); } fn get_node(&self, index: usize) -> Option { self.nodes.borrow().get(index).map(|n| n.value) } } } ``` Le borrow checker peut sembler restrictif au début, mais ces contraintes éliminent des catégories entières de bugs présents dans d'autres langages. ## Conclusion Les entretiens Rust évaluent une compréhension profonde du système de propriété, des garanties de sécurité mémoire, et de la capacité à écrire du code concurrent sans data races. Maîtriser ces concepts distingue les développeurs qui peuvent tirer parti des avantages uniques de Rust. ### Checklist de préparation - ✅ Comprendre ownership, borrowing et les trois règles fondamentales - ✅ Savoir quand et comment annoter les lifetimes - ✅ Maîtriser les traits et la différence entre dispatch statique et dynamique - ✅ Gérer les erreurs idiomatiquement avec Result et Option - ✅ Choisir le bon smart pointer selon le contexte - ✅ Écrire du code concurrent avec Arc, Mutex et channels - ✅ Comprendre async/await et les runtimes comme tokio - ✅ Savoir quand et comment utiliser unsafe de manière sûre La préparation aux entretiens Rust nécessite de la pratique avec les concepts de propriété uniques au langage. Les exercices sur Exercism, les projets personnels, et la contribution à l'écosystème Rust consolident ces connaissances pour réussir les entretiens techniques les plus exigeants. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/rust/questions-entretien-rust