Tutoriels gratuits et ouverts à toutes et à tous · conçus au Lycée Chevrollier — AngersÀ propos

Série « Parcours Rust » · partie 2 / 8

Rust niveau 1 — Types, fonctions et contrôle

Choisir le bon type, découper en fonctions, décider et répéter : niveau 1 du parcours Rust, avec exercices corrigés et QCM.

Développement✎ B. Jousseaume📅 17 octobre 2026⏱ 5 h📶 Débutant

1. Les types de base

Rust est statiquement typé : le type de chaque valeur est connu à la compilation. Il est souvent déduit, mais il existe toujours.

TypeContenuExemple
i8 i16 i32 i64 i128Entiers signés (le nombre = nombre de bits)let t: i32 = -12;
u8 u16 u32 u64 u128Entiers non signés (jamais négatifs)let age: u8 = 19;
usize / isizeTaille de l’adresse machine : sert aux indices et aux longueurslet i: usize = 3;
f32 / f64Nombres décimaux (virgule flottante)let pi = 3.14159;
booltrue ou falselet admin = false;
charUn caractère Unicode, entre apostropheslet c = 'é';
  • Sans indication, un entier est un i32 et un décimal un f64.
  • Le séparateur _ rend les grands nombres lisibles : 1_000_000.
  • Un char s’écrit entre apostrophes, un texte entre guillemets : 'a' n’est pas "a".
TypeMinimumMaximum
u80255
i8−128127
u3204 294 967 295
i32−2 147 483 6482 147 483 647
u640environ 1,8 × 10¹⁹

2. Le dépassement d’entier

Que se passe-t-il si l’on dépasse le maximum d’un type ?

fn main() {
    let niveau: u8 = 255;
    let suivant = niveau + 1;
    println!("{suivant}");
}
error: this arithmetic operation will overflow
 --> src/main.rs:3:19
  |
3 |     let suivant = niveau + 1;
  |                   ^^^^^^^^^^ attempt to compute `u8::MAX + 1_u8`, which would overflow

Ici la valeur est connue à l’avance : le compilateur refuse directement le programme. Quand elle ne l’est pas (une saisie, un calcul), le comportement dépend du mode de compilation.

ModeComportement
cargo run (debug)Le programme s’arrête avec attempt to add with overflow
cargo run --releaseLa valeur reboucle silencieusement : 255 + 1 donne 0

Quand un dépassement est possible, on dit explicitement ce que l’on veut :

fn main() {
    let a: u8 = 250;
    println!("{:?}", a.checked_add(10));     // None : le résultat ne tient pas
    println!("{:?}", a.checked_add(3));      // Some(253)
    println!("{}", a.saturating_add(10));    // 255 : bloqué au maximum
    println!("{}", a.wrapping_add(10));      // 4 : rebouclage voulu
}

Some et None seront étudiés au niveau 3 : retiens pour l’instant que None signifie « pas de résultat valide ».

Convertir un nombre

fn main() {
    let grand: i32 = 300;
    let tronque = grand as u8;              // 44 : conversion brutale, sans avertissement
    println!("{tronque}");

    let sur = u8::try_from(grand);          // conversion vérifiée
    println!("{:?}", sur);                  // Err(TryFromIntError(()))

    let entier = 7;
    let decimal = entier as f64 / 2.0;      // 3.5 : int vers flottant, sans risque
    println!("{decimal}");
}

3. Tuples et tableaux

fn main() {
    // Tuple : des valeurs de types différents, en nombre fixe
    let eleve = ("Lina", 19, 14.5);
    println!("{} a {} ans et {} de moyenne", eleve.0, eleve.1, eleve.2);
    let (nom, age, moyenne) = eleve;           // déstructuration
    println!("{nom} {age} {moyenne}");

    // Tableau : des valeurs du même type, en nombre fixe
    let notes = [12, 15, 9, 17, 11];
    let zeros = [0; 10];                       // dix zéros
    println!("première : {}, nombre : {}", notes[0], notes.len());
    println!("{:?}", zeros);
}

Le type d’un tableau contient sa taille : [i32; 5]. Pour des listes qui grandissent, on utilisera les vecteurs au niveau 4.

Si l’on demande une case qui n’existe pas avec un indice écrit en dur, le compilateur refuse le programme. Quand l’indice n’est connu qu’à l’exécution (une saisie, un calcul), le programme s’arrête :

fn note_numero(notes: &[i32], i: usize) -> i32 {
    notes[i]
}

fn main() {
    let notes = [12, 15, 9, 17, 11];
    println!("{}", note_numero(&notes, 7));
}
thread 'main' panicked at src/main.rs:2:5:
index out of bounds: the len is 5 but the index is 7

4. Les fonctions

fn aire_rectangle(longueur: f64, largeur: f64) -> f64 {
    longueur * largeur          // pas de point-virgule : c'est la valeur renvoyée
}

fn afficher_titre(texte: &str) {
    println!("=== {texte} ===");
}

fn main() {
    afficher_titre("Calcul");
    let a = aire_rectangle(12.5, 4.0);
    println!("Aire : {a}");
}
  • Le type de chaque paramètre est obligatoire.
  • Le type de retour s’écrit après ->. Sans flèche, la fonction ne renvoie rien.
  • &str désigne un texte emprunté ; le niveau 2 explique le &.
  • L’ordre de déclaration des fonctions dans le fichier n’a pas d’importance.

Instructions et expressions

En Rust, presque tout est une expression qui produit une valeur. Un bloc { } vaut sa dernière expression, à condition qu’elle ne soit pas suivie d’un point-virgule.

fn carre(x: i32) -> i32 {
    x * x              // expression : renvoyée
}

fn carre_faux(x: i32) -> i32 {
    x * x;             // instruction : le bloc ne vaut rien → erreur E0308
}

return existe aussi, pour sortir avant la fin de la fonction : if x < 0 { return 0; }.

5. Décider : if et match

fn main() {
    let note = 13;
    if note >= 16 {
        println!("Très bien");
    } else if note >= 10 {
        println!("Validé");
    } else {
        println!("À retravailler");
    }

    // if est une expression : il produit une valeur
    let mention = if note >= 10 { "admis" } else { "ajourné" };
    println!("{mention}");
}
  • La condition n’a pas de parenthèses et doit être un bool : if note { } est refusé, contrairement à Python ou C.
  • Opérateurs : ==, !=, <, >, <=, >=, et && (et), || (ou), ! (non).
  • Dans un if qui produit une valeur, toutes les branches doivent produire le même type.

match compare une valeur à une série de motifs. Il est exhaustif : tous les cas doivent être couverts, sinon le programme ne compile pas.

fn main() {
    let note = 13;
    let appreciation = match note {
        0..=4 => "très insuffisant",
        5..=9 => "insuffisant",
        10..=13 => "correct",
        14..=16 => "bien",
        17..=20 => "excellent",
        _ => "note invalide",             // tout le reste
    };
    println!("{appreciation}");

    let jour = 6;
    match jour {
        1 | 2 | 3 | 4 | 5 => println!("jour de cours"),
        6 | 7 => println!("week-end"),
        _ => println!("jour inconnu"),
    }
}

6. Répéter : loop, while, for

fn main() {
    // for sur un intervalle : 1 à 5 inclus
    for i in 1..=5 {
        print!("{i} ");
    }
    println!();

    // for sur un tableau
    let notes = [12, 15, 9];
    for n in notes {
        println!("note : {n}");
    }

    // à rebours, avec l'indice
    for i in (1..4).rev() {
        println!("{i}…");
    }

    // while
    let mut capital = 1000.0;
    let mut annees = 0;
    while capital < 2000.0 {
        capital *= 1.05;
        annees += 1;
    }
    println!("Capital doublé en {annees} ans");

    // loop : boucle infinie, dont on sort avec break (qui peut renvoyer une valeur)
    let mut n = 1;
    let premier_carre_sup_100 = loop {
        if n * n > 100 {
            break n * n;
        }
        n += 1;
    };
    println!("{premier_carre_sup_100}");
}
ÉcritureValeurs parcourues
0..50, 1, 2, 3, 4 (5 exclu)
0..=50 à 5 inclus
(0..5).rev()4, 3, 2, 1, 0
(0..10).step_by(2)0, 2, 4, 6, 8

continue passe au tour suivant, break sort de la boucle. Pour sortir de deux boucles imbriquées, on nomme la boucle extérieure : 'externe: for … puis break 'externe;.

7. Lire une saisie au clavier

use std::io;

fn main() {
    println!("Quel âge as-tu ?");
    let mut saisie = String::new();
    io::stdin()
        .read_line(&mut saisie)
        .expect("lecture impossible");

    let age: u32 = match saisie.trim().parse() {
        Ok(valeur) => valeur,
        Err(_) => {
            println!("Ce n'est pas un nombre valide.");
            return;
        }
    };
    println!("Dans 10 ans, tu auras {} ans.", age + 10);
}
  • String::new() crée un texte vide et modifiable, dans lequel read_line écrit la saisie.
  • trim() retire le retour à la ligne final, sinon la conversion échoue.
  • parse() tente la conversion : elle réussit (Ok) ou échoue (Err), et le match traite les deux cas.
  • Ce programme ne fonctionne pas dans le Playground, qui ne permet pas la saisie : utilise cargo run.

8. Mini-projet : le juste prix

9. Bilan

Exercices corrigés

Les erreurs de ce niveau viennent presque toujours d’un mélange de types (entier et décimal, signé et non signé) ou d’un point-virgule en trop à la fin d’une fonction. Lis le message du compilateur avant de chercher ailleurs.

Exercice 1.1 — Types et limites

Voir le corrigé
fn main() {
    // 1.
    let x: u8 = 200;
    println!("{:?}", x.checked_add(100));     // None : 300 ne tient pas dans un u8
    println!("{}", x.saturating_add(100));    // 255 : bloqué au maximum
    println!("{}", x.wrapping_add(100));      // 44 : 300 - 256

    // 2.
    println!("{} {} {}", i32::MAX, u64::MAX, i8::MIN);

    // 3.
    let moins_un: i32 = -1;
    println!("{}", moins_un as u32);          // 4294967295
    println!("{:?}", u32::try_from(moins_un)); // Err(TryFromIntError(()))

    // 4.
    let c = 'é';
    println!("{c} occupe {} octets", std::mem::size_of::<char>());
}

Explication

Le rebouclage calcule le résultat modulo 256 pour un u8 : 300 − 256 = 44. La conversion de −1 avec as réinterprète les bits : le résultat est le plus grand u32, une valeur absurde et silencieuse. Un char occupe 4 octets car il peut représenter n’importe quel caractère Unicode, emoji compris.

Exercice 1.2 — Fonctions et boucles

Voir le corrigé
fn est_pair(n: i32) -> bool {
    n % 2 == 0
}

fn factorielle(n: u64) -> u64 {
    let mut resultat = 1;
    for i in 2..=n {
        resultat *= i;
    }
    resultat
}

fn factorielle_sure(n: u64) -> Option<u64> {
    let mut resultat: u64 = 1;
    for i in 2..=n {
        resultat = resultat.checked_mul(i)?;   // ? : sort avec None en cas de dépassement
    }
    Some(resultat)
}

fn somme_chiffres(mut n: u32) -> u32 {
    let mut somme = 0;
    while n > 0 {
        somme += n % 10;
        n /= 10;
    }
    somme
}

fn main() {
    for i in 1..=20 {
        if est_pair(i) {
            print!("{i} ");
        }
    }
    println!();

    println!("20! = {}", factorielle(20));
    println!("20! = {:?}", factorielle_sure(20));
    println!("21! = {:?}", factorielle_sure(21));
    println!("{}", somme_chiffres(1234));

    for i in 1..=30 {
        match (i % 3, i % 5) {
            (0, 0) => println!("FizzBuzz"),
            (0, _) => println!("Fizz"),
            (_, 0) => println!("Buzz"),
            _ => println!("{i}"),
        }
    }
}

Explication

20! est la plus grande factorielle qui tient dans un u64. Le ? après checked_mul fait sortir la fonction avec None dès qu’une multiplication déborde : c’est une écriture que l’on retrouvera beaucoup au niveau 4. Le match sur un tuple teste deux conditions à la fois de manière très lisible ; _ signifie « n’importe quelle valeur ».

Exercice 1.3 — Saisie contrôlée

Voir le corrigé
use std::io;

fn main() {
    let note: u32 = loop {
        println!("Note sur 20 :");
        let mut saisie = String::new();
        io::stdin().read_line(&mut saisie).expect("lecture impossible");

        let valeur: u32 = match saisie.trim().parse() {
            Ok(v) => v,
            Err(_) => {
                println!("Ce n'est pas un nombre entier positif.");
                continue;
            }
        };
        if valeur > 20 {
            println!("La note doit être entre 0 et 20.");
            continue;
        }
        break valeur;
    };

    let appreciation = match note {
        0..=4 => "très insuffisant",
        5..=9 => "insuffisant",
        10..=13 => "correct",
        14..=16 => "bien",
        _ => "excellent",
    };
    println!("{note}/20 : {appreciation}");
}

Explication

Le loop produit directement la valeur validée grâce à break valeur. Le contrôle se fait en deux temps : le type (u32 refuse le texte et les négatifs), puis les bornes métier (au plus 20). Le dernier cas du second match peut être _ car la note est garantie inférieure ou égale à 20.

Mini-projet

Voir le corrigé
use std::io;
use std::time::{SystemTime, UNIX_EPOCH};

const ESSAIS_MAX: u32 = 7;

fn nombre_secret() -> u32 {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("horloge système invalide")
        .subsec_nanos();
    nanos % 100 + 1
}

fn lire_proposition() -> Option<u32> {
    let mut saisie = String::new();
    io::stdin().read_line(&mut saisie).ok()?;
    match saisie.trim().parse::<u32>() {
        Ok(n) if (1..=100).contains(&n) => Some(n),
        _ => None,
    }
}

fn main() {
    let secret = nombre_secret();
    let mut essais = 0;
    println!("J'ai choisi un nombre entre 1 et 100. Tu as {ESSAIS_MAX} essais.");

    while essais < ESSAIS_MAX {
        println!("Essai {} / {ESSAIS_MAX} :", essais + 1);
        let proposition = match lire_proposition() {
            Some(n) => n,
            None => {
                println!("Saisis un nombre entier entre 1 et 100.");
                continue;
            }
        };
        essais += 1;

        if proposition < secret {
            println!("Plus grand !");
        } else if proposition > secret {
            println!("Plus petit !");
        } else {
            println!("Gagné en {essais} essai(s) !");
            return;
        }
    }
    println!("Perdu ! Le nombre était {secret}.");
}

Explication

Le programme est découpé en fonctions qui ont chacune un rôle : produire le secret, lire une proposition valide, jouer. lire_proposition renvoie None pour toute saisie invalide ; la condition if (1..=100).contains(&n) placée dans le motif s’appelle une garde. Le compteur n’augmente qu’après une saisie valide.

Pour aller plus loin

Avec 7 essais et la stratégie « couper en deux », on trouve toujours un nombre entre 1 et 100 : 2⁷ = 128 > 100. C’est la recherche dichotomique, que tu retrouveras en algorithmique.

🧠 Testez vos connaissances

5 questions. Répondez puis cliquez sur « Corriger ». Rien n’est enregistré : c’est de l’auto-évaluation.

1. Que se passe-t-il quand un u8 valant 255 reçoit + 1 dans un programme compilé en mode debug ?

💬 En debug, Rust vérifie les dépassements. En release, la valeur reboucle à 0 : checked_add rend le comportement explicite dans les deux modes.

2. Que renvoie 300_i32 as u8 ?

💬 as tronque sans vérifier. u8::try_from(300) renverrait une erreur, ce qui est préférable pour une valeur venant de l’extérieur.

3. Que renvoie cette fonction ? fn f(x: i32) -> i32 { x * 2; }

💬 Avec un point-virgule, x * 2 devient une instruction : le bloc ne produit plus de valeur. On écrit x * 2 sans point-virgule.

4. Quelles valeurs parcourt for i in 1..5 ?

💬 La borne de droite est exclue. 1..=5 inclut le 5.

5. Pourquoi match refuse-t-il de compiler s’il manque des cas ?

💬 L’exhaustivité garantit qu’aucun cas n’est oublié. _ est un moyen de couvrir « tout le reste », pas une obligation.

© 2026 Bruno Jousseaume. Ce cours est publié sous licence CC BY-NC-SA 4.0 : vous pouvez le partager et l’adapter en citant l’auteur et un lien vers cette page, sans usage commercial, et en partageant vos adaptations sous la même licence.

Pour aller plus loin

Python niveau 1 — Premiers pas

Afficher, calculer, mémoriser dans des variables et dialoguer avec l’utilisateur : niveau 1 du parcours Python, avec exercices corrigés et QCM.

Débutant⏱ 4 à 5 h · 17 octobre 2026