Annotations de types
Il existe de nombreuses situations où l’annotation explicite est obligatoire (comme dans les signatures de fonctions) ou utile pour guider le compilateur.
Voici toutes les façons d’annoter des types en Rust, classées par contexte.
Les variables locales (let)
L’annotation se place après le nom de la variable, séparée par deux-points (:).
// Syntaxe standard
let x: u8 = 42;
// Avec mutabilité (le modificateur 'mut' se place avant le nom, pas le type)
let mut y: f64 = 3.14;
// Avec du destructuring (on annote le motif complet)
let (tuple_a, tuple_b): (bool, char) = (true, 'R');
Les fonctions et méthodes
En Rust, l’inférence de types ne s’applique pas aux signatures de fonctions publiques ou privées. Nous devons obligatoirement annoter les types des paramètres et du retour.
// Les paramètres et le type de retour (après '->')
fn additionner(a: f64, b: f64) -> f64 {
a + b
}
Nota : si les types des opérandes et ou d’une expression ne correspondent pas, le compilateur génère une erreur de type.
Le cas des fonctions anonymes (closures)
Les closures profitent de l’inférence tout comme les fonctions.
L’annotation est très similaire à celle des fonctions :
let closure_annotee = |x: i32| -> i32 { x + 1 };
Les constantes et static
Contrairement aux variables let, les constantes (const) et les variables globales (static) requièrent obligatoirement une annotation de type explicite.
const SEUIL_MAX: u32 = 100_000;
static NOM_APPLICATION: &str = "MonApp";
Les Littéraux
Nous pouvons attacher le type directement comme suffixe d’une valeur numérique littérale.
let entier = 42u8; // Type u8
let long = 1000i64; // Type i64
let flottant = 3.0f32; // Type f32
Pour améliorer la lisibilité, Rust permet d’utiliser le caractère _ comme séparateur visuel. Il est possible de l’insérer juste avant le suffixe ou au milieu des chiffres pour séparer les grands nombres :
let entier = 42_u8;
let flottant = 3.0_f32;
let grand_nombre = 1_000_i64;
Le turbofish (::<>)
L’inférence ne suffit pas toujours à savoir vers quel type nous souhaitons convertir la donnée. Le Turbofish permet de passer des types à une fonction comme s’il s’agissait d’arguments.
Sa syntaxe est identifiant::<Type>.
// Signifie : "Parse cette chaîne en un entier i32"
let nombre = "42".parse::<i32>().unwrap();
// Signifie : "Collecte les éléments de l'itérateur dans un Vec de chaînes"
let liste = vec![1, 2, 3].into_iter().map(|x| x.to_string()).collect::<Vec<String>>();
Nous aurions aussi pu écrire
let liste: Vec<String> = ...collect();. Le choix entre l’annotation de variable et le turbofish est une question de choix personnels en termes de lisibilité et de style.
L’opérateur de transtypage (as)
Pour convertir un type primitif vers un autre (quand la conversion est sûre et supportée), on utilise le mot-clé as. Cela applique une conversion tout en forçant le type final.
let valeur_i32: i32 = 10;
let valeur_f64 = valeur_i32 as f64; // Convertit le i32 en f64
Les types génériques et contraintes
Dans les structures, les énumérations ou les blocs d’implémentation, les types peuvent être annotés sous forme de paramètres génériques, souvent accompagnés de contraintes (trait bounds).
// Annotation d'un paramètre générique T dans une structure
struct Conteneur<T> {
valeur: T,
}
// Annotation avec contrainte : T doit obligatoirement implémenter le trait Display
fn afficher_element<T: std::fmt::Display>(element: T) {
println!("{}", element);
}
// Syntaxe alternative avec la clause 'where' (plus lisible pour les signatures lourdes)
fn traiter<T>(element: T)
where
T: Clone + Debug
{
// ...
}
L’annotation de masquage partiel (_)
Si vous devez annoter un type complexe (comme un type générique composé) mais que vous voulez laisser le compilateur deviner une partie de ce type, vous pouvez utiliser le caractère de soulignement _.
// On dit à Rust : "C'est un Vec, mais devine ce qu'il y a dedans"
let scores: Vec<_> = vec![10, 20, 30];