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.

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.
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.
// 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.
// 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.
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.
// 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.
// 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<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// Syntaxe alternative avec where
fn notify_verbose<T>(item: &T)
where
T: Summary,
{
println!("Breaking news! {}", item.summarize());
}
// Multiples trait bounds
fn notify_complex<T: Summary + Clone + std::fmt::Display>(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).
// 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<T: Animal>(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<Box<dyn Animal>> = 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<T>(&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é.
Prêt à réussir tes entretiens Rust ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
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<T, E> et Option<T> avec le pattern matching.
// Gestion idiomatique des erreurs en Rust
use std::fs::File;
use std::io::{self, Read};
// Option<T> : présence ou absence de valeur
fn find_user(id: u32) -> Option<String> {
match id {
1 => Some(String::from("Alice")),
2 => Some(String::from("Bob")),
_ => None, // Pas d'utilisateur trouvé
}
}
// Result<T, E> : succès ou erreur
fn divide(a: f64, b: f64) -> Result<f64, String> {
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<dyn std::error::Error>> {
// 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<String, io::Error> {
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<io::Error> for AppError {
fn from(error: io::Error) -> Self {
AppError::IoError(error)
}
}
fn complex_operation() -> Result<String, AppError> {
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.
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.
// 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<Self, DataStoreError> {
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<Record, DataStoreError> {
// 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<Config> {
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.
// Les principaux smart pointers de Rust
use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;
// BOX<T> : 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<List>),
Nil,
}
let list = List::Cons(1,
Box::new(List::Cons(2,
Box::new(List::Cons(3,
Box::new(List::Nil))))));
println!("{:?}", list);
}
// RC<T> : 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<T> : 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<T> : 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<RefCell<T>>
// Plusieurs propriétaires avec mutation possible
fn rc_refcell_example() {
#[derive(Debug)]
struct Node {
value: i32,
children: Vec<Rc<RefCell<Node>>>,
}
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<Mutex<T>> ou Arc<RwLock<T>>
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.
// 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.
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.
// 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<String, reqwest::Error> {
// 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.
Prêt à réussir tes entretiens Rust ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
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.
// 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<String>,
}
// 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<String>,
}
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<String>) -> 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<String>) -> 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<Server, String> {
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<Server, String> {
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<State = Unvalidated> {
url: String,
method: String,
headers: Vec<(String, String)>,
_state: std::marker::PhantomData<State>,
}
impl Request<Unvalidated> {
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<Request<Validated>, 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<Validated> {
// send() n'est disponible que sur les requêtes validées
pub async fn send(self) -> Result<Response, reqwest::Error> {
// 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.
// 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<i32> { ... } // ERREUR
// SOLUTION 1 : Newtype wrapper
struct Wrapper(Vec<String>);
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<String>;
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<T> {
fn first_or_default(&self) -> Option<&T>;
}
impl<T> VecExt<T> for Vec<T> {
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<Self, &'static str> {
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.
// 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<User> {
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.
// 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<T> {
ptr: *mut T,
len: usize,
capacity: usize,
}
impl<T> MyVec<T> {
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<T> Drop for MyVec<T> {
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::<T>(self.capacity).unwrap();
std::alloc::dealloc(self.ptr as *mut u8, layout);
}
}
}
}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.
// 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<i32>,
field2: Vec<i32>,
}
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<i32> = 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<Vec<Node>>,
}
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<i32> {
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
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
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.
Tags
Partager
Articles similaires

Ownership et Borrowing en Rust : Guide complet
Maîtrisez le système d'ownership et borrowing de Rust. Comprendre les règles de propriété, références, lifetimes et les patterns avancés de gestion mémoire.

Rust : Les bases pour développeurs expérimentés en 2026
Apprenez Rust rapidement en partant de vos acquis. Ownership, borrowing, lifetimes et patterns essentiels expliqués pour développeurs venant de C++, Java ou Python.

Ownership et Borrowing en Rust : le guide complet pour les entretiens techniques
Comprendre en profondeur l'ownership, le borrowing, les lifetimes et le borrow checker en Rust. Un guide essentiel pour se preparer aux entretiens techniques systemes.