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.

Guide Rust pour développeurs expérimentés

Rust gagne en popularité chaque année, et pour cause : sécurité mémoire garantie à la compilation, performances équivalentes au C++, et un écosystème moderne. Pour un développeur expérimenté venant de C++, Java ou Python, l'apprentissage de Rust peut sembler déroutant au début, mais les concepts fondamentaux deviennent rapidement intuitifs une fois compris.

Pourquoi Rust en 2026 ?

Rust est le langage le plus apprécié sur Stack Overflow depuis 8 ans consécutifs. Adopté par Microsoft, Google, Amazon et Meta pour des composants critiques, il offre la sécurité mémoire sans garbage collector.

Configuration de l'environnement Rust

Avant de coder, il faut installer Rust via rustup, l'outil officiel de gestion des versions.

bash
# install.sh
# Installation de Rust via rustup (macOS, Linux)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Vérification de l'installation
rustc --version
cargo --version

Cargo est le gestionnaire de paquets et outil de build de Rust. Il combine les fonctionnalités de npm, Maven et Make en un seul outil cohérent.

bash
# project-setup.sh
# Création d'un nouveau projet
cargo new mon_projet
cd mon_projet

# Structure générée :
# mon_projet/
# ├── Cargo.toml    # Manifeste (équivalent package.json)
# └── src/
#     └── main.rs   # Point d'entrée

# Commandes essentielles
cargo build          # Compile le projet
cargo run            # Compile et exécute
cargo test           # Lance les tests
cargo check          # Vérifie sans compiler (plus rapide)

Variables et immutabilité par défaut

Rust inverse la convention habituelle : les variables sont immutables par défaut. Cette approche force à réfléchir explicitement à la mutabilité et prévient de nombreux bugs.

variables.rsrust
fn main() {
    // Variable immutable par défaut
    let x = 5;
    // x = 6;  // Erreur de compilation !

    // Variable mutable explicite
    let mut y = 5;
    y = 6;  // OK

    // Shadowing : redéclaration dans le même scope
    let x = x + 1;  // Crée une nouvelle variable x
    let x = x * 2;  // x vaut maintenant 12

    // Le shadowing permet aussi de changer le type
    let spaces = "   ";         // &str
    let spaces = spaces.len();  // usize
}
Shadowing vs mutabilité

Le shadowing crée une nouvelle variable, contrairement à mut qui modifie la valeur existante. Le shadowing permet de transformer une valeur tout en gardant un nom clair.

Types de données fondamentaux

Rust est statiquement typé avec une excellente inférence de types. Voici les types primitifs essentiels à connaître.

types.rsrust
fn main() {
    // Entiers signés : i8, i16, i32, i64, i128, isize
    let age: i32 = 30;

    // Entiers non signés : u8, u16, u32, u64, u128, usize
    let count: u64 = 1_000_000;  // Underscores pour lisibilité

    // Flottants : f32, f64 (défaut)
    let pi: f64 = 3.14159;

    // Booléen
    let active: bool = true;

    // Caractère Unicode (4 bytes)
    let emoji: char = '🦀';

    // Tuple : collection fixe de types différents
    let person: (String, i32) = (String::from("Alice"), 28);
    let (name, age) = person;  // Destructuring

    // Array : taille fixe, même type
    let numbers: [i32; 5] = [1, 2, 3, 4, 5];
    let first = numbers[0];

    // Slice : vue sur une portion de données
    let slice: &[i32] = &numbers[1..4];  // [2, 3, 4]
}

Ownership : le concept révolutionnaire

L'ownership est LA innovation de Rust. Ce système garantit la sécurité mémoire sans garbage collector, au prix d'une courbe d'apprentissage initiale.

Les trois règles de l'ownership

ownership.rsrust
fn main() {
    // Règle 1 : Chaque valeur a un unique propriétaire
    let s1 = String::from("hello");

    // Règle 2 : Quand le propriétaire sort du scope, la valeur est libérée
    {
        let s2 = String::from("world");
        // s2 est valide ici
    }
    // s2 est libéré (drop), plus accessible

    // Règle 3 : Une seule ownership à la fois (move)
    let s3 = s1;  // s1 est MOVE vers s3
    // println!("{}", s1);  // Erreur : s1 n'est plus valide !
    println!("{}", s3);     // OK : s3 est le propriétaire
}

Move vs Clone

move_clone.rsrust
fn main() {
    // Types simples (stack) : Copy automatique
    let x = 5;
    let y = x;  // Copy, pas move
    println!("x = {}, y = {}", x, y);  // Les deux sont valides

    // Types complexes (heap) : Move par défaut
    let s1 = String::from("hello");
    let s2 = s1;  // Move
    // s1 n'est plus utilisable

    // Clone explicite pour dupliquer
    let s3 = String::from("world");
    let s4 = s3.clone();  // Deep copy
    println!("s3 = {}, s4 = {}", s3, s4);  // Les deux valides
}
Move et fonctions

Passer une valeur à une fonction transfère l'ownership. La fonction devient propriétaire et la valeur n'est plus accessible après l'appel, sauf si elle est retournée.

ownership_functions.rsrust
fn main() {
    let s = String::from("hello");
    takes_ownership(s);
    // println!("{}", s);  // Erreur : s a été moved

    let x = 5;
    makes_copy(x);
    println!("{}", x);  // OK : i32 implémente Copy

    // Pour récupérer l'ownership, retourner la valeur
    let s2 = String::from("hello");
    let s3 = takes_and_gives_back(s2);
    println!("{}", s3);  // OK
}

fn takes_ownership(s: String) {
    println!("{}", s);
}  // s est dropped ici

fn makes_copy(x: i32) {
    println!("{}", x);
}

fn takes_and_gives_back(s: String) -> String {
    s  // Retourne l'ownership
}

Borrowing : références sans transfert

Le borrowing permet d'utiliser une valeur sans en prendre l'ownership. C'est le mécanisme le plus utilisé en Rust.

borrowing.rsrust
fn main() {
    let s1 = String::from("hello");

    // Référence immutable : lecture seule
    let len = calculate_length(&s1);
    println!("Longueur de '{}' : {}", s1, len);  // s1 toujours valide

    // Référence mutable : modification possible
    let mut s2 = String::from("hello");
    change(&mut s2);
    println!("{}", s2);  // "hello, world"
}

fn calculate_length(s: &String) -> usize {
    s.len()
}  // s sort du scope mais ne drop pas car c'est une référence

fn change(s: &mut String) {
    s.push_str(", world");
}

Règles du borrowing

borrowing_rules.rsrust
fn main() {
    let mut s = String::from("hello");

    // Règle 1 : Plusieurs références immutables simultanées OK
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);

    // Règle 2 : UNE SEULE référence mutable à la fois
    let r3 = &mut s;
    // let r4 = &mut s;  // Erreur : déjà emprunté mutably
    println!("{}", r3);

    // Règle 3 : Pas de ref mutable si ref immutable existe
    let r5 = &s;
    // let r6 = &mut s;  // Erreur : r5 est encore actif
    println!("{}", r5);

    // Une fois r5 utilisé pour la dernière fois, on peut emprunter mut
    let r7 = &mut s;  // OK : r5 n'est plus utilisé après
    r7.push_str("!");
}

Prêt à réussir tes entretiens Rust ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Lifetimes : garantir la validité des références

Les lifetimes s'assurent que les références restent valides. Le compilateur les infère souvent automatiquement, mais parfois une annotation explicite est nécessaire.

lifetimes_basic.rsrust
// Erreur classique : référence vers donnée libérée
// fn dangling() -> &String {
//     let s = String::from("hello");
//     &s  // Erreur : s sera dropped, référence invalide !
// }

// Solution : retourner la valeur owned
fn no_dangle() -> String {
    let s = String::from("hello");
    s  // Ownership transféré, pas de problème
}

Annotations de lifetimes

lifetimes_annotation.rsrust
// Le compilateur ne peut pas deviner quelle référence sera retournée
// fn longest(x: &str, y: &str) -> &str { ... }  // Erreur !

// Annotation explicite : le retour vit aussi longtemps que x ET y
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    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 dropped
}
Élision des lifetimes

Rust applique des règles d'élision pour éviter d'annoter les cas simples. Pour les débutants, il suffit de suivre les messages du compilateur qui sont très explicites.

Structs et implémentations

Les structs sont les blocs de construction pour les types personnalisés en Rust.

structs.rsrust
// Définition d'une struct
#[derive(Debug)]  // Permet d'afficher avec {:?}
struct User {
    username: String,
    email: String,
    active: bool,
    sign_in_count: u64,
}

// Bloc d'implémentation pour les méthodes
impl User {
    // Constructeur (convention : fn new ou fn nom_explicite)
    fn new(username: String, email: String) -> Self {
        Self {
            username,
            email,
            active: true,
            sign_in_count: 1,
        }
    }

    // Méthode : prend &self (référence à l'instance)
    fn is_active(&self) -> bool {
        self.active
    }

    // Méthode avec mutation : prend &mut self
    fn deactivate(&mut self) {
        self.active = false;
    }

    // Méthode consommant self (rare)
    fn into_username(self) -> String {
        self.username
    }
}

fn main() {
    let mut user = User::new(
        String::from("alice"),
        String::from("alice@example.com"),
    );

    println!("Active: {}", user.is_active());
    user.deactivate();
    println!("Active: {}", user.is_active());

    // Debug print
    println!("{:?}", user);
}

Enums et Pattern Matching

Les enums Rust sont bien plus puissants que dans la plupart des langages : chaque variant peut contenir des données.

enums.rsrust
// Enum simple
enum Direction {
    North,
    South,
    East,
    West,
}

// Enum avec données (algebraic data type)
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

impl Message {
    fn process(&self) {
        match self {
            Message::Quit => println!("Quitting"),
            Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
            Message::Write(text) => println!("Writing: {}", text),
            Message::ChangeColor(r, g, b) => {
                println!("Color: rgb({}, {}, {})", r, g, b)
            }
        }
    }
}

fn main() {
    let msg = Message::Move { x: 10, y: 20 };
    msg.process();

    let msg2 = Message::Write(String::from("Hello Rust!"));
    msg2.process();
}

Option et Result : gestion des erreurs

option_result.rsrust
use std::fs::File;
use std::io::{self, Read};

fn main() {
    // Option<T> : valeur présente ou absente (remplace null)
    let numbers = vec![1, 2, 3];
    let first: Option<&i32> = numbers.first();

    match first {
        Some(n) => println!("Premier : {}", n),
        None => println!("Liste vide"),
    }

    // Méthodes utilitaires
    let value = first.unwrap_or(&0);
    let doubled = first.map(|n| n * 2);

    // Result<T, E> : succès ou erreur
    let result = read_file("config.txt");
    match result {
        Ok(content) => println!("Contenu : {}", content),
        Err(e) => println!("Erreur : {}", e),
    }
}

fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;  // ? propage l'erreur
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    Ok(content)
}
L'opérateur ?

L'opérateur ? est du sucre syntaxique pour propager les erreurs. Il retourne automatiquement l'erreur si Result est Err, sinon il unwrap la valeur Ok.

Traits : polymorphisme à la Rust

Les traits définissent un comportement partagé, similaires aux interfaces Java ou aux protocols Swift.

traits.rsrust
// Définition d'un trait
trait Summary {
    fn summarize(&self) -> String;

    // Méthode avec implémentation par défaut
    fn preview(&self) -> String {
        format!("{}...", &self.summarize()[..50.min(self.summarize().len())])
    }
}

struct Article {
    title: String,
    author: String,
    content: String,
}

struct Tweet {
    username: String,
    content: String,
}

// Implémentation pour Article
impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{} by {}", self.title, self.author)
    }
}

// Implémentation pour Tweet
impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.username, self.content)
    }
}

// Fonction acceptant tout type implémentant Summary
fn notify(item: &impl Summary) {
    println!("Breaking news: {}", item.summarize());
}

// Syntaxe équivalente avec trait bound
fn notify_generic<T: Summary>(item: &T) {
    println!("Breaking news: {}", item.summarize());
}

fn main() {
    let article = Article {
        title: String::from("Rust 2026"),
        author: String::from("Community"),
        content: String::from("..."),
    };

    let tweet = Tweet {
        username: String::from("rustlang"),
        content: String::from("Rust is awesome!"),
    };

    notify(&article);
    notify(&tweet);
}

Collections essentielles

Rust fournit des collections puissantes dans la bibliothèque standard.

collections.rsrust
use std::collections::HashMap;

fn main() {
    // Vec<T> : tableau dynamique
    let mut numbers: Vec<i32> = Vec::new();
    numbers.push(1);
    numbers.push(2);
    numbers.push(3);

    // Macro vec! pour initialisation
    let nums = vec![1, 2, 3, 4, 5];

    // Itération
    for n in &nums {
        println!("{}", n);
    }

    // Méthodes fonctionnelles
    let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();
    let sum: i32 = nums.iter().sum();
    let evens: Vec<&i32> = nums.iter().filter(|x| *x % 2 == 0).collect();

    // String : chaîne UTF-8 growable
    let mut s = String::from("Hello");
    s.push_str(", World!");
    s.push('!');

    // Concaténation
    let s1 = String::from("Hello, ");
    let s2 = String::from("World!");
    let s3 = s1 + &s2;  // s1 moved, s2 borrowed
    // ou avec format!
    let s4 = format!("{}{}", "Hello, ", "World!");

    // HashMap<K, V>
    let mut scores: HashMap<String, i32> = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Red"), 50);

    // Accès avec get (retourne Option)
    if let Some(score) = scores.get("Blue") {
        println!("Blue: {}", score);
    }

    // Entry API pour insertion conditionnelle
    scores.entry(String::from("Yellow")).or_insert(25);
}

Gestion des erreurs idiomatique

Une bonne gestion des erreurs est essentielle en Rust. Voici les patterns recommandés.

error_handling.rsrust
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;

// Définir un type d'erreur custom
#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
    Custom(String),
}

// Implémenter From pour conversion automatique
impl From<io::Error> for AppError {
    fn from(err: io::Error) -> Self {
        AppError::Io(err)
    }
}

impl From<ParseIntError> for AppError {
    fn from(err: ParseIntError) -> Self {
        AppError::Parse(err)
    }
}

// Fonction retournant Result avec erreur custom
fn read_number_from_file(path: &str) -> Result<i32, AppError> {
    let mut file = File::open(path)?;  // io::Error -> AppError
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    let number: i32 = content.trim().parse()?;  // ParseIntError -> AppError
    Ok(number)
}

fn main() {
    match read_number_from_file("number.txt") {
        Ok(n) => println!("Number: {}", n),
        Err(AppError::Io(e)) => println!("IO error: {}", e),
        Err(AppError::Parse(e)) => println!("Parse error: {}", e),
        Err(AppError::Custom(msg)) => println!("Error: {}", msg),
    }
}
Crates recommandées

Pour des projets réels, les crates thiserror (pour les libraries) et anyhow (pour les applications) simplifient grandement la gestion des erreurs.

Tests en Rust

Rust intègre un framework de test directement dans le langage.

lib.rsrust
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

pub fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("Division by zero"))
    } else {
        Ok(a / b)
    }
}

// Module de tests (compilé uniquement pour `cargo test`)
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn test_add_negative() {
        assert_eq!(add(-1, 1), 0);
    }

    #[test]
    fn test_divide_success() {
        assert_eq!(divide(10, 2), Ok(5));
    }

    #[test]
    fn test_divide_by_zero() {
        assert!(divide(10, 0).is_err());
    }

    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn test_panic() {
        let v = vec![1, 2, 3];
        let _ = v[99];  // Panic !
    }
}

Conclusion

Rust offre un paradigme unique qui combine sécurité mémoire et performances. Les concepts d'ownership et de borrowing semblent contraignants au début, mais deviennent naturels avec la pratique. Le compilateur Rust est un allié précieux : ses messages d'erreur sont parmi les meilleurs de l'industrie.

Checklist pour bien démarrer

  • ✅ Installer Rust via rustup et maîtriser Cargo
  • ✅ Comprendre la différence entre immutabilité et mutabilité explicite
  • ✅ Maîtriser les trois règles de l'ownership
  • ✅ Pratiquer le borrowing avec références & et &mut
  • ✅ Utiliser Option et Result au lieu de null et exceptions
  • ✅ Écrire des tests avec #[test]

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

La communauté Rust est accueillante et les ressources abondantes. Le livre officiel "The Rust Programming Language" est disponible gratuitement en ligne. Avec ces bases solides, vous êtes prêt à explorer des sujets avancés comme async/await, les macros et WebAssembly.

Tags

#rust
#systems programming
#ownership
#memory safety
#performance

Partager

Articles similaires