Développement Web

Variables, types et fonctions en Rust

12 min de lecture

📍 Guide principal de la série : Apprendre Rust de zéro : le guide complet pour débuter. Ce tutoriel fait partie de la série « Rust de zéro ». Pour la vue d’ensemble, lisez d’abord le guide principal.

Des variables qui ne changent pas par défaut

Vous venez d’installer Rust et de créer le projet stock-cli. Place maintenant à la matière première de tout programme : les données. En Rust, une variable est immuable par défaut — une fois une valeur posée, elle ne bouge plus, sauf si vous demandez explicitement le contraire. Ce choix surprend les développeurs venus de Python ou de JavaScript, mais c’est l’une des décisions qui rend les programmes Rust si fiables : le compilateur vous empêche de modifier par accident une donnée que vous pensiez figée.

Dans ce tutoriel, vous apprenez à déclarer des variables, à choisir le bon type, à écrire des fonctions et à structurer la logique de stock-cli. Concrètement, vous allez représenter un produit de l’atelier d’Ousmane — un écran de rechange, par exemple — avec son prix et sa quantité, puis calculer la valeur du stock.

🎯 Ce que vous allez apprendre

  • Déclarer des variables immuables et mutables avec let et let mut, et utiliser le shadowing.
  • Choisir parmi les types scalaires (i32, u32, f64, bool, char) et composés (tuples, tableaux).
  • Écrire des fonctions avec paramètres typés et valeur de retour, et distinguer instruction et expression.
  • Contrôler le flux avec if, loop, while et for, sachant qu’en Rust if est une expression.

🛠️ Ce que vous allez construire

Un petit programme qui décrit un produit (nom, prix unitaire, quantité en stock), calcule la valeur totale du stock via une fonction dédiée, et affiche un récapitulatif. C’est la logique de calcul qui servira de noyau à stock-cli dans les tutoriels suivants.

Prérequis

  • La toolchain Rust installée et le projet stock-cli créé (voir le tutoriel précédent).
  • Test express : si cargo run affiche déjà un message dans votre terminal, vous êtes prêt.
  • ⏱️ Temps estimé : ~35 minutes.

Étape 1 — Déclarer des variables : immuable, mutable, shadowing

On commence par le mot-clé let, qui lie un nom à une valeur. Par défaut, cette liaison est immuable. Pour autoriser la modification, on ajoute mut. Ouvrez src/main.rs et écrivez :

fn main() {
    let nom_produit = "Écran iPhone 11";   // immuable
    let mut quantite = 8;                    // mutable : le stock va varier
    println!("Produit : {nom_produit}, quantité : {quantite}");

    quantite = quantite - 1;                 // une vente : on décrémente
    println!("Après une vente : {quantite}");
}

Si vous aviez oublié le mut sur quantite, le compilateur refuserait la réaffectation avec un message clair : cannot assign twice to immutable variable. Ce n’est pas une punition, c’est une garantie : toute donnée modifiable est signalée par mut, donc visible d’un coup d’œil.

Rust offre un autre mécanisme, le shadowing : redéclarer une variable avec let crée une nouvelle variable qui masque l’ancienne, éventuellement avec un type différent. C’est très pratique pour transformer une donnée tout en gardant un nom parlant :

    let prix = "4500";                  // une chaîne, type &str
    let prix: u32 = prix.parse().unwrap(); // nouvelle variable, type u32
    println!("Prix en FCFA : {prix}");

Le shadowing diffère de mut : ici on crée une nouvelle variable (on peut changer de type), alors que mut modifie la même variable sans changer son type. Distinguez bien les deux : c’est une source de confusion fréquente chez les débutants.

Point d’étape — Lancez cargo run. Vous devez voir le nom, la quantité initiale, la quantité après vente et le prix. Si vous obtenez cannot assign twice to immutable variable, c’est que le mut manque sur la variable réaffectée.

Étape 2 — Choisir le bon type scalaire

Rust est statiquement typé : chaque valeur a un type connu à la compilation. Le compilateur infère souvent le type, mais comprendre les familles disponibles vous évite des erreurs de débordement et des pertes de précision. Les types scalaires se rangent en quatre familles, que voici appliquées à stock-cli :

    let quantite: u32 = 8;        // entier non signé (jamais négatif)
    let solde: i32 = -1500;       // entier signé (peut être négatif)
    let prix_unitaire: f64 = 4500.0; // flottant double précision
    let en_promo: bool = true;    // booléen
    let categorie: char = 'É';    // un seul caractère Unicode

Le choix signé/non signé n’est pas cosmétique. Une quantité en stock ne peut pas être négative : u32 exprime cette intention et empêche les bugs. Un solde de caisse, lui, peut passer dans le rouge : i32. Pour l’argent, attention : f64 introduit de minuscules erreurs d’arrondi ; pour des montants exacts en FCFA, on travaille souvent en entiers (le prix en francs, sans décimales), puisque le franc CFA n’a pas de centimes.

Un point important sur les entiers : en mode debug, un débordement (dépasser la capacité du type) fait paniquer le programme, ce qui vous alerte. En mode release, le calcul « repart à zéro » silencieusement (wrapping). Pour un code robuste, on utilise des méthodes explicites comme checked_sub qui renvoient une option au lieu de déborder — on y reviendra avec la gestion d’erreurs.

Étape 3 — Regrouper des données : tuples et tableaux

Un produit, c’est plusieurs valeurs liées. Plutôt que trois variables éparpillées, on peut les regrouper. Le tuple assemble des valeurs de types éventuellement différents :

    // (nom, prix unitaire, quantité)
    let ecran = ("Écran iPhone 11", 4500u32, 8u32);
    println!("{} coûte {} FCFA", ecran.0, ecran.1);

    // déstructuration : on extrait les trois champs d'un coup
    let (nom, prix, qte) = ecran;
    println!("{nom} — {qte} en stock");

On accède aux éléments d’un tuple par leur position (.0, .1, .2) ou par déstructuration. Le tuple est parfait pour un retour de fonction à plusieurs valeurs, mais dès que la structure se complexifie, on passera aux struct (sujet d’un prochain tutoriel) qui nomment chaque champ.

Le tableau (array), lui, regroupe des valeurs de même type, en nombre fixe connu à la compilation :

    let stock_faible = [3u32, 1, 0, 5]; // 4 quantités
    println!("Premier seuil : {}", stock_faible[0]);
    println!("Nombre de seuils : {}", stock_faible.len());

Le tableau a une taille figée. Pour une liste qui grandit (ajouter des produits au fil de l’eau), on utilisera plus tard le Vec, un tableau dynamique. Notez que Rust vérifie les accès : lire stock_faible[10] sur un tableau de 4 éléments fait paniquer le programme avec un message explicite, là où d’autres langages liraient une zone mémoire au hasard.

Point d’étape — Affichez le nom et le prix via le tuple, puis la longueur du tableau. Si le compilateur signale mismatched types, vérifiez que toutes les valeurs du tableau sont bien du même type.

Étape 4 — Écrire des fonctions : paramètres, retour, expressions

Une fonction encapsule une logique réutilisable. C’est ici que Rust révèle une subtilité essentielle : la distinction entre instruction (statement, qui fait quelque chose sans renvoyer de valeur) et expression (qui produit une valeur). Écrivons la fonction centrale de stock-cli, qui calcule la valeur d’un stock :

// prend deux entiers, renvoie leur produit (valeur du stock)
fn valeur_stock(prix_unitaire: u32, quantite: u32) -> u32 {
    prix_unitaire * quantite   // pas de point-virgule = c'est la valeur de retour
}

fn main() {
    let total = valeur_stock(4500, 8);
    println!("Valeur du stock : {total} FCFA");
}

Trois points méritent attention. D’abord, chaque paramètre est typé (prix_unitaire: u32) : Rust n’infère jamais le type des paramètres, c’est volontaire et cela documente la fonction. Ensuite, le type de retour suit la flèche ->. Enfin, et c’est le point clé : la dernière ligne prix_unitaire * quantite n’a pas de point-virgule. En Rust, une expression sans point-virgule en fin de fonction est la valeur renvoyée. Ajouter un ; en ferait une instruction qui ne renvoie rien, et le compilateur protesterait : mismatched types: expected u32, found ().

Vous pouvez aussi utiliser le mot-clé return pour sortir plus tôt, mais idiomatiquement, on s’appuie sur l’expression finale. Cette logique « tout est expression » se retrouve partout, y compris dans le if.

Étape 5 — Contrôler le flux : if, boucles et for

En Rust, if est une expression : il produit une valeur, qu’on peut affecter directement. Cela évite les ternaires obscurs. Voyons une règle métier de stock-cli : signaler un réapprovisionnement quand le stock est bas.

fn etat_stock(quantite: u32) -> &'static str {
    if quantite == 0 {
        "RUPTURE"
    } else if quantite < 3 {
        "À réapprovisionner"
    } else {
        "OK"
    }
}

Chaque branche du if renvoie une chaîne, et la fonction retourne directement le résultat. Toutes les branches doivent renvoyer le même type — sinon, erreur de compilation. C’est encore une garantie : impossible d’oublier un cas qui renverrait autre chose.

Pour répéter, Rust offre trois boucles. loop tourne indéfiniment jusqu’à un break (qui peut renvoyer une valeur) ; while boucle tant qu’une condition est vraie ; for parcourt une collection ou un intervalle. La plus utilisée, et de loin, est for :

fn main() {
    let quantites = [8u32, 2, 0, 5];
    for q in quantites {
        println!("Quantité {q} -> {}", etat_stock(q));
    }

    // un intervalle : 1, 2, 3
    for n in 1..=3 {
        println!("Passage {n}");
    }
}

La syntaxe 1..=3 est un intervalle inclusif (1 à 3) ; 1..3 serait exclusif (1 et 2). Le for de Rust parcourt directement les éléments, sans index manuel ni risque de dépassement : plus sûr et plus lisible que la boucle à compteur classique.

Point d’étape — Lancez le programme : il doit afficher l’état de chaque quantité (OK, À réapprovisionner, RUPTURE) puis les trois passages. Si une branche du if renvoie un type différent, corrigez : toutes les branches doivent s’accorder.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
cannot assign twice to immutable variable Réaffectation sans mut Déclarer avec let mut
mismatched types: expected u32, found () Point-virgule de trop sur l’expression de retour Retirer le ; final
cannot find value... in this scope Variable utilisée hors de son bloc Déclarer la variable au bon niveau
attempt to subtract with overflow (debug) Soustraction sous zéro sur un u32 Utiliser i32 ou checked_sub
if and else have incompatible types Branches du if de types différents Uniformiser le type renvoyé par chaque branche

🌍 Adaptation au contexte ouest-africain

Le choix des types prend tout son sens avec la monnaie locale. Le franc CFA (FCFA) ne se divise pas en centimes : un prix s’exprime en entiers. Stocker un prix en f64 (flottant) pour des francs est non seulement inutile mais risqué — les arrondis flottants peuvent transformer 4500 en 4499,9999. Travaillez en u32 (ou u64 pour de gros montants) et vos totaux seront exacts au franc près. Cette rigueur compte dès qu’on facture un client ou qu’on fait une caisse en fin de journée.

Même réflexe pour les quantités : un compteur de stock ne descend jamais sous zéro dans la réalité. Le type u32 encode cette règle, et le débordement détecté en mode debug vous prévient si une vente de trop fait passer le stock dans le négatif — un bug métier que d’autres langages laisseraient filer silencieusement.

✅ Récapitulatif

Vous savez désormais déclarer des variables immuables et mutables, jouer du shadowing, choisir un type scalaire adapté (signé ou non, entier ou flottant), regrouper des données avec tuples et tableaux, écrire des fonctions typées qui renvoient une expression, et piloter le flux avec if-expression et les trois boucles. Vous avez surtout construit le noyau de calcul de stock-cli : valeur_stock et etat_stock. Le prochain défi est le plus fondamental de Rust : comprendre qui « possède » les données, c’est-à-dire l’ownership.

🧾 Aide-mémoire

Élément Rôle
let x = ... Variable immuable
let mut x = ... Variable mutable
let x: u32 = ... Annotation de type explicite
u32 / i32 / f64 / bool / char Types scalaires de base
(a, b, c) / [a, b, c] Tuple / tableau de taille fixe
fn f(x: T) -> U { ... } Fonction typée ; expression finale = retour
1..=5 Intervalle inclusif pour for

💪 À vous de jouer

Écrivez une fonction prix_ttc(prix_ht: u32) -> u32 qui ajoute la TVA sénégalaise de 18 % à un prix hors taxe, et affichez le prix TTC d’un écran à 4500 FCFA. Astuce : multipliez par 118 puis divisez par 100, en restant en entiers.

Voir une solution
fn prix_ttc(prix_ht: u32) -> u32 {
    prix_ht * 118 / 100   // +18 % de TVA, calcul en entiers
}

fn main() {
    let ttc = prix_ttc(4500);
    println!("Prix TTC : {ttc} FCFA"); // 5310 FCFA
}

On multiplie d’abord (4500 * 118 = 531000) avant de diviser par 100, pour éviter de perdre de la précision avec une division entière prématurée.

Tutoriels frères

Pour aller plus loin

FAQ

Q : Pourquoi les variables sont-elles immuables par défaut ?
R : Pour la sûreté et la lisibilité. Une donnée qui ne change pas ne peut pas être corrompue par accident, et le compilateur peut mieux optimiser. Quand la mutation est nécessaire, mut la rend visible.

Q : Quand utiliser un tuple plutôt qu’un tableau ?
R : Le tuple regroupe des valeurs de types différents en nombre fixe (un enregistrement). Le tableau regroupe des valeurs de même type. Pour une liste qui grandit, on utilisera plus tard un Vec.

Q : Faut-il toujours annoter les types ?
R : Non. Rust infère le type dans la plupart des cas. On annote pour les paramètres de fonction (obligatoire), pour lever une ambiguïté (comme parse), ou pour documenter une intention.

Q : Quelle est la différence entre String et &str ?
R : &str est une vue immuable sur une chaîne (souvent un littéral), String est une chaîne possédée et modifiable, allouée sur le tas. La distinction prendra tout son sens avec l’ownership, sujet du prochain tutoriel.

Partager