Bases du langage
Présentation
Rust est un langage de programmation compilé, à typage statique fort, créé par Graydon Hoare chez Mozilla et dont la première version stable (1.0) est sortie en 2015. Il est aujourd’hui maintenu par la Rust Foundation.
Son objectif principal est d’offrir les performances du C et du C++ tout en garantissant la sûreté mémoire à la compilation, sans ramasse-miettes (garbage collector).
Ses principales caractéristiques sont :
- Sûreté mémoire garantie : le compilateur vérifie, via le système d’ownership, qu’aucun accès invalide à la mémoire n’est possible (pas de pointeur nul, pas de use after free, pas de dépassement de tampon).
- Pas de ramasse-miettes : la mémoire est libérée de façon déterministe, à la fin de la portée de la valeur qui la détient.
- Concurrence sans data race : les mêmes règles qui garantissent la sûreté mémoire empêchent, à la compilation, deux threads d’écrire simultanément dans la même donnée. C’est ce que la communauté appelle la fearless concurrency.
- Abstractions sans coût (zero-cost abstractions) : les itérateurs, les génériques ou les traits sont résolus à la compilation et n’induisent aucun surcoût à l’exécution.
- Immutabilité par défaut : une variable ne peut pas être modifiée sauf si on le demande explicitement.
- Absence de
nullet d’exceptions : l’absence de valeur et l’échec sont représentés par des types de l’énumération standard,Option<T>etResult<T, E>, que le compilateur oblige à traiter.
Info
Rust est utilisé aussi bien pour des outils système (le noyau Linux accepte des pilotes en Rust depuis la version 6.1) que pour du développement web back-end, de l’embarqué, du WebAssembly ou de l’outillage (les outils front-end modernes comme SWC ou Rolldown sont écrits en Rust).
Installation
L’installation se fait via rustup, le gestionnaire officiel de chaînes d’outils (toolchains).
Télécharger et exécuter rustup-init.exe depuis rustup.rs.
L’installation fournit plusieurs binaires :
rustc: le compilateurcargo: le gestionnaire de paquets et de buildrustup: le gestionnaire de versions de la toolchainrustfmt: le formateur de codeclippy: le linter
Commandes utiles de rustup
Note
Rust suit un cycle de publication de 6 semaines sur trois canaux : stable, beta et nightly. Les évolutions majeures du langage sont regroupées dans des éditions (2015, 2018, 2021, 2024) qui sont déclarées par projet. Une édition ne casse pas la compatibilité : des crates d’éditions différentes peuvent cohabiter dans un même binaire.
Cargo
cargo est l’outil central de l’écosystème : il gère la compilation, les dépendances, les tests et la documentation.
Un projet créé par cargo new a la structure suivante :
Le fichier Cargo.toml décrit le projet et ses dépendances :
Tip
Structure d’un programme
Le point d’entrée d’un exécutable est la fonction main.
Info
println! n’est pas une fonction mais une macro, ce qu’indique le point d’exclamation. Les macros sont développées à la compilation, ce qui permet ici de vérifier le format et les arguments dès la compilation.
Afficher et journaliser
Les macros d’affichage
Toutes reposent sur la même syntaxe de formatage et se distinguent par leur destination.
| Macro | Destination | Usage |
|---|---|---|
print! / println! | Sortie standard | Ce que le programme produit |
eprint! / eprintln! | Sortie d’erreur | Diagnostics, avertissements, progression |
format! | Une String | Construire une chaîne sans l’afficher |
write! / writeln! | Un flux quelconque | Fichiers, tampons, implémentations de Display |
dbg! | Sortie d’erreur | Débogage temporaire |
Tip
La distinction entre les deux sorties n’est pas cosmétique : elle permet de rediriger les résultats sans y mêler les diagnostics.
Tout ce qui n’est pas le résultat utile du programme — progression, avertissements, erreurs — appartient à la sortie d’erreur.
dbg! affiche le fichier, la ligne, l’expression et sa valeur, puis renvoie la valeur — ce qui permet de l’intercaler dans une expression sans rien réécrire :
Contrairement à println!, il est fait pour être retiré : sa présence dans un dépôt signale un oubli, et Clippy le signale via dbg_macro.
La syntaxe {}
C’est le point central. Une paire d’accolades est un emplacement que la macro remplit, selon une syntaxe qui suit toujours la forme {argument:spécification}.
Désigner l’argument
La capture directe {nom} ne fonctionne qu’avec un nom de variable, jamais avec une expression : {utilisateur.nom} ou {v[0]} ne compilent pas. Il faut alors passer l’expression en argument, ou la lier au préalable.
Pour afficher une accolade littérale, on la double :
{} ou {:?} : deux traits différents
C’est la distinction la plus structurante.
| Forme | Trait | Destinataire |
|---|---|---|
{} | Display | L’utilisateur — lisible, sans détail d’implémentation |
{:?} | Debug | Le développeur — structure interne |
{:#?} | Debug | Idem, sur plusieurs lignes indentées |
Debug s’obtient par #[derive(Debug)] et devrait figurer sur presque tous les types. Display, lui, s’écrit à la main : c’est un choix éditorial, que le compilateur ne peut pas deviner. Les deux traits sont détaillés dans Les traits.
Warning
Un type sans Display ne peut pas s’afficher avec {} — c’est le cas de Vec, HashMap, Option et de toutes les collections, qui n’ont que Debug. L’erreur E0277 doesn't implement std::fmt::Display se corrige presque toujours en passant à {:?}.
La spécification de format
Après le : viennent, dans l’ordre, le remplissage et l’alignement, le signe, la largeur, la précision et le type.
La largeur et la précision peuvent elles-mêmes être des arguments, en les suffixant de $ :
C’est ce qui permet d’aligner un tableau dont la largeur des colonnes n’est connue qu’à l’exécution.
Le formatage emprunte toujours
Point essentiel au regard des règles de propriété : les macros de formatage prennent leurs arguments par référence. Afficher une valeur ne la consomme donc jamais.
Toutes les méthodes des traits de formatage prennent &self, ce qui est cohérent : afficher quelque chose ne devrait pas le détruire. Le mécanisme est développé dans Ownership et emprunt.
Journalisation
println! convient à un script ou à un exemple. Dès qu’un programme tourne en production, il lui manque l’essentiel : niveaux de gravité, horodatage, filtrage à l’exécution, sortie structurée.
Deux écosystèmes coexistent.
log est une façade minimale : la bibliothèque émet des messages, l’application choisit l’implémentation qui les traite — env_logger, simplelog, fern.
tracing va plus loin en introduisant la notion de span : un intervalle de temps auquel des champs sont attachés, ce qui permet de suivre une requête à travers plusieurs fonctions, y compris en asynchrone.
La syntaxe des champs se lit ainsi : % emploie Display, ? emploie Debug, et l’absence de préfixe suppose le type natif.
| Besoin | Choix |
|---|---|
| Bibliothèque | log — laisse le choix de l’implémentation à l’appelant |
| Application simple | log + env_logger |
| Application asynchrone, tracé distribué | tracing + tracing-subscriber |
Note
Les cinq niveaux sont communs aux deux écosystèmes, du plus fin au plus grave : trace, debug, info, warn, error. Le filtrage se fait à l’exécution par la variable d’environnement RUST_LOG, et les messages sous le seuil retenu ne coûtent presque rien — leurs arguments ne sont même pas évalués.
Commentaires
Variables et mutabilité
Une variable est déclarée avec let et est immutable par défaut. Le mot-clé mut la rend modifiable.
Le type est généralement inféré, mais peut être annoté explicitement :
Shadowing
Redéclarer une variable avec let crée une nouvelle variable qui masque la précédente. Contrairement à mut, cela permet de changer de type.
Constantes
Types de données
Types scalaires
| Catégorie | Types | Exemples |
|---|---|---|
| Entiers signés | i8, i16, i32, i64, i128, isize | -42, 1_000_000 |
| Entiers non signés | u8, u16, u32, u64, u128, usize | 42, 0xff, 0b1010, b'A' |
| Flottants | f32, f64 | 3.14, 2.5e3 |
| Booléen | bool | true, false |
| Caractère | char | 'a', 'é', '🦀' |
Note
Le type par défaut est i32 pour les entiers et f64 pour les flottants. Un char occupe 4 octets et représente un scalaire Unicode, pas un octet.
Types composés
Chaînes de caractères
Rust distingue deux types de chaînes, toutes deux encodées en UTF-8 :
&str: une tranche de chaîne, empruntée et de taille fixe. C’est le type des littéraux.String: une chaîne allouée sur le tas, redimensionnable et possédée.
Warning
Une String n’est pas indexable par entier (chaine[0] ne compile pas) : en UTF-8, un caractère peut occuper plusieurs octets. Il faut passer par .chars(), .bytes() ou une tranche d’octets &chaine[0..4].
Fonctions
Le type des paramètres et du retour est toujours obligatoire. Le mot-clé return existe mais n’est utilisé que pour les retours anticipés.
Info
Rust distingue les instructions (statements), qui ne renvoient pas de valeur et se terminent par un point-virgule, des expressions, qui en renvoient une. Un bloc { } est lui-même une expression, ce qui permet d’écrire :
Structures de contrôle
Conditions
if est une expression, ce qui remplace l’opérateur ternaire.
Note
La condition doit obligatoirement être un bool : contrairement au C, if nombre { ... } ne compile pas.
Boucles
Rust propose trois formes de boucles, sans équivalent du do...while :
Filtrage par motif
match compare une valeur à une série de motifs. Il est exhaustif : le compilateur refuse le code si un cas n’est pas traité.
Pour ne traiter qu’un seul motif, if let est plus concis :
Info
Les étiquettes de boucle, les blocs étiquetés, le type divergent ! et la sortie anticipée dans une chaîne d’itérateurs sont traités dans Contrôle de flux et expressions. La syntaxe complète des motifs est détaillée dans Déstructuration et patterns.
Ownership et emprunt
C’est le concept central du langage, celui qui permet de se passer d’un ramasse-miettes. Il repose sur trois règles vérifiées à la compilation :
- Chaque valeur a une variable qui la possède : son owner.
- Il ne peut y avoir qu’un seul owner à la fois.
- Quand l’owner sort de sa portée, la valeur est libérée.
Info
Les types de taille fixe stockés sur la pile (entiers, flottants, booléens, char, tuples de ces types) implémentent le trait Copy : ils sont copiés au lieu d’être déplacés. L’affectation let a = b; reste donc valide pour un i32.
Pour utiliser une valeur sans en prendre la propriété, on l’emprunte avec une référence &.
Un emprunt mutable &mut permet de modifier la valeur empruntée :
Warning
La règle d’emprunt est stricte : à un instant donné, on peut avoir soit un nombre illimité de références immutables, soit une seule référence mutable, jamais les deux. C’est cette règle qui élimine les data races à la compilation.
Gestion de l’absence et des erreurs
Rust n’a ni null ni exceptions. Ces deux cas sont modélisés par des énumérations de la bibliothèque standard, que match oblige à traiter.
L’opérateur ? propage automatiquement l’erreur à la fonction appelante, ce qui évite d’imbriquer les match :
Tip
Les méthodes unwrap() et expect("message") extraient la valeur directement mais provoquent un panic! en cas d’absence ou d’erreur. Elles sont pratiques pour un prototype ou un test, mais à éviter en production.
Ressources
- The Rust Programming Language : le livre de référence, gratuit et en ligne (traduction française)
- Rust by Example : apprentissage par l’exemple, avec code exécutable
- Rustlings : exercices interactifs
- Rust Playground : compilation et exécution dans le navigateur
- Standard Library : documentation de la bibliothèque standard