Types et variables#

Rust est un langage à typage statique : le type de chaque valeur est connu à la compilation. Le compilateur peut souvent inférer le type à partir du contexte, mais il est toujours possible — et parfois nécessaire — de l’annoter explicitement.

Types scalaires#

Un type scalaire représente une valeur unique. Rust possède quatre familles de types scalaires : les entiers, les flottants, les booléens et les caractères.

Entiers#

Définition 13 (Types entiers)

Un type entier spécifie un entier de taille fixe, signé ou non signé. La notation iN désigne un entier signé sur N bits ; la notation uN désigne un entier non signé sur N bits. Les types isize et usize ont la taille d’un pointeur sur l’architecture cible.

La table suivante récapitule l’ensemble des types entiers disponibles.

Type

Taille (bits)

Valeur minimale

Valeur maximale

i8

8

\(-128\)

\(127\)

i16

16

\(-32\,768\)

\(32\,767\)

i32

32

\(-2\,147\,483\,648\)

\(2\,147\,483\,647\)

i64

64

\(-2^{63}\)

\(2^{63} - 1\)

i128

128

\(-2^{127}\)

\(2^{127} - 1\)

u8

8

\(0\)

\(255\)

u16

16

\(0\)

\(65\,535\)

u32

32

\(0\)

\(4\,294\,967\,295\)

u64

64

\(0\)

\(2^{64} - 1\)

u128

128

\(0\)

\(2^{128} - 1\)

isize

arch.

dépend de la cible

dépend de la cible

usize

arch.

\(0\)

dépend de la cible

Proposition 1 (Taille et représentation des entiers)

Les entiers signés sont représentés en complément à deux. Un entier signé sur \(n\) bits peut contenir toute valeur dans l’intervalle \([-2^{n-1},\; 2^{n-1} - 1]\). Un entier non signé sur \(n\) bits couvre \([0,\; 2^n - 1]\).

Le type entier par défaut, lorsque le compilateur ne dispose d’aucune autre indication, est i32. Les types isize et usize sont principalement utilisés pour l’indexation des collections.

Exemple 9 (Déclaration d’entiers)

Voir le code ci-dessous.

let x: i32 = 42;
let y: u8 = 255;
let z: isize = -1;

// Les littéraux peuvent utiliser des séparateurs visuels
let million = 1_000_000;

// Suffixe de type sur le littéral
let a = 42u64;

println!("x = {x}, y = {y}, z = {z}");
println!("million = {million}, a = {a}");
x = 42, y = 255, z = -1
million = 1000000, a = 42

Les littéraux entiers acceptent plusieurs bases d’écriture :

let decimal = 98_222;
let hexadecimal = 0xff;
let octal = 0o77;
let binaire = 0b1111_0000;
let octet: u8 = b'A';  // syntaxe byte

println!("déc={decimal}, hex={hexadecimal}, oct={octal}, bin={binaire}, octet={octet}");
déc=98222, hex=255, oct=63, bin=240, octet=65

Flottants#

Définition 14 (Types flottants)

Rust fournit deux types à virgule flottante conformes à la norme IEEE 754 : f32 (simple précision, 32 bits) et f64 (double précision, 64 bits). Le type par défaut est f64, car sur les processeurs modernes, sa vitesse est comparable à celle de f32 tout en offrant une précision supérieure.

Exemple 10 (Opérations sur les flottants)

Voir le code ci-dessous.

let x = 2.0;       // f64 par défaut
let y: f32 = 3.14; // f32 explicite

println!("x = {x}, y = {y}");
println!("x + y as f64 = {}", x + y as f64);
println!("f64::INFINITY = {}", f64::INFINITY);
println!("f64::NAN = {}", f64::NAN);
println!("NAN == NAN ? {}", f64::NAN == f64::NAN); // false !
x = 2, y = 3.14
x + y as f64 = 5.140000104904175
f64::INFINITY = inf
f64::NAN = NaN
NAN == NAN ? false

Remarque 10

La valeur NAN (Not a Number) n’est égale à aucune valeur, pas même à elle-même. Pour tester si une valeur est NAN, on utilise la méthode is_nan().

Booléens#

Définition 15 (Type booléen)

Le type bool possède exactement deux valeurs : true et false. Il occupe un octet en mémoire. Les booléens sont le type de retour naturel des expressions de comparaison et des conditions.

let vrai = true;
let faux: bool = false;

println!("vrai = {vrai}, faux = {faux}");
println!("taille de bool = {} octet", std::mem::size_of::<bool>());
vrai = true, faux = false
taille de bool = 1 octet

Caractères#

Définition 16 (Type caractère)

Le type char représente un scalaire Unicode (Unicode Scalar Value). Il occupe 4 octets en mémoire et peut représenter tout point de code Unicode valide, de U+0000 à U+D7FF et de U+E000 à U+10FFFF.

Remarque 11

Un char en Rust n’est pas un octet comme en C. Il représente un scalaire Unicode complet, ce qui inclut les lettres accentuées, les idéogrammes CJK et les émojis. Les littéraux char s’écrivent entre apostrophes simples.

Exemple 11 (Caractères Unicode)

Voir le code ci-dessous.

let lettre = 'a';
let accent = 'é';
let ideogramme = '漢';
let emoji = '🦀';

println!("{lettre}, {accent}, {ideogramme}, {emoji}");
println!("taille de char = {} octets", std::mem::size_of::<char>());
println!("'é' en Unicode = U+{:04X}", accent as u32);
a, é, 漢, 🦀
taille de char = 4 octets
'é' en Unicode = U+00E9

Types composés#

Les types composés regroupent plusieurs valeurs en une seule entité. Rust possède deux types composés primitifs : les tuples et les tableaux.

Tuples#

Définition 17 (Tuple)

Un tuple est une collection ordonnée de valeurs de types éventuellement différents, de longueur fixe. La syntaxe est (T1, T2, ..., Tn). On accède aux éléments par index (notation pointée) ou par destructuration.

Exemple 12 (Utilisation des tuples)

Voir le code ci-dessous.

let triplet: (i32, f64, bool) = (42, 6.28, true);

// Accès par index
println!("premier  = {}", triplet.0);
println!("deuxième = {}", triplet.1);
println!("troisième = {}", triplet.2);

// Destructuration
let (a, b, c) = triplet;
println!("a = {a}, b = {b}, c = {c}");
premier  = 42
deuxième = 6.28
troisième = true
a = 42, b = 6.28, c = true

Tableaux#

Définition 18 (Tableau)

Un tableau (array) est une collection de valeurs de même type, de taille fixe connue à la compilation. Le type s’écrit [T; N]T est le type des éléments et N leur nombre. Les tableaux sont alloués sur la pile (stack).

Exemple 13 (Déclaration et manipulation de tableaux)

Voir le code ci-dessous.

let nombres: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 10]; // dix zéros

println!("premier = {}", nombres[0]);
println!("dernier = {}", nombres[4]);
println!("longueur = {}", nombres.len());
println!("zeros = {:?}", zeros);
premier = 1
dernier = 5
longueur = 5
zeros = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Définition 19 (Tranche (slice))

Une tranche (slice) est une vue sur une portion contiguë d’un tableau (ou de toute séquence). Son type est &[T]. Elle ne possède pas les données ; elle emprunte une référence vers elles. Une tranche stocke un pointeur et une longueur.

{
let tableau = [10, 20, 30, 40, 50];

let tranche: &[i32] = &tableau[1..4]; // éléments d'index 1, 2, 3
println!("tranche = {:?}", tranche);
println!("longueur de la tranche = {}", tranche.len());

let tout: &[i32] = &tableau[..]; // tranche complète
println!("tout = {:?}", tout);
}
tranche = [20, 30, 40]
longueur de la tranche = 3
tout = [10, 20, 30, 40, 50]
()

Le type String vs &str#

Définition 20 (String et &str)

  • String est une chaîne de caractères possédée, allouée sur le tas (heap), redimensionnable. Elle est encodée en UTF-8.

  • &str est une tranche de chaîne (string slice), c’est-à-dire une référence immuable vers une séquence d’octets UTF-8. Les littéraux de chaîne ("bonjour") sont de type &str.

Remarque 12

La distinction entre String et &str est fondamentale en Rust. Elle reflète le modèle de possession : String possède ses données et les libère quand elle sort de la portée, tandis que &str emprunte des données possédées par un autre propriétaire. On peut penser à String comme le type « lecture-écriture » et à &str comme le type « lecture seule ».

Exemple 14 (Création et conversion)

Voir le code ci-dessous.

{
// Création de String
let s1 = String::from("bonjour");
let s2 = "monde".to_string();
let s3: String = format!("{} {}", s1, s2);

println!("{s3}");

// De String vers &str : emprunt automatique (deref coercion)
let tranche: &str = &s3;
println!("tranche = {tranche}");

// De &str vers String : allocation
let s4: String = tranche.to_owned();
println!("s4 = {s4}");
}
bonjour monde
tranche = bonjour monde
s4 = bonjour monde
()
// Les chaînes sont en UTF-8 : attention à l'indexation
let salut = String::from("Héllo");
println!("longueur en octets = {}", salut.len());      // 6 (é = 2 octets)
println!("nombre de caractères = {}", salut.chars().count()); // 5

// Itération par caractères
for c in salut.chars() {
    print!("{c} ");
}
println!();
longueur en octets = 6
nombre de caractères = 5
H é l l o 

Conversions de types#

Transtypage avec as#

Le mot-clé as effectue un transtypage (cast) entre types primitifs. Cette conversion peut être avec perte d’information.

let x: i32 = 42;
let y: f64 = x as f64;
let z: u8 = 200;
let w: i8 = z as i8; // troncature : 200 > i8::MAX

println!("x = {x}, y = {y}, z = {z}, w = {w}");
x = 42, y = 42, z = 200, w = -56

Traits From et Into#

Définition 21 (From et Into)

Le trait From<T> définit une conversion infaillible d’un type T vers le type implémenteur. Le trait Into<U> en est le dual : si From<T> est implémenté pour U, alors Into<U> est automatiquement disponible pour T.

Exemple 15 (Conversions avec From et Into)

Voir le code ci-dessous.

// From : conversion explicite
let s = String::from("texte");
let n: i64 = i64::from(42i32);

// Into : conversion implicite guidée par le type attendu
let m: i64 = 42i32.into();

println!("s = {s}, n = {n}, m = {m}");
s = texte, n = 42, m = 42

Traits TryFrom et TryInto#

Définition 22 (TryFrom et TryInto)

Les traits TryFrom<T> et TryInto<U> définissent des conversions faillibles. Elles retournent un Result<T, E>, ce qui permet de gérer proprement les cas d’échec (par exemple, la conversion d’un entier trop grand pour le type cible).

Exemple 16 (Conversions faillibles)

Voir le code ci-dessous.

let grand: i32 = 1_000;

// TryInto retourne un Result
let petit: Result<u8, _> = grand.try_into();
println!("1000 en u8 ? {:?}", petit); // Err(...)

let correct: i32 = 42;
let ok: Result<u8, _> = correct.try_into();
println!("42 en u8 ? {:?}", ok); // Ok(42)
1000 en u8 ? Err(TryFromIntError(()))
42 en u8 ? Ok(42)

Le type unité ()#

Définition 23 (Type unité)

Le type unité () est un tuple vide. Il ne contient aucune donnée et n’occupe aucun espace en mémoire. C’est le type de retour implicite des fonctions et des blocs qui ne retournent pas de valeur significative.

fn afficher(msg: &str) {
    println!("{msg}");
    // retourne implicitement ()
}

let resultat: () = afficher("bonjour");
println!("type unité : {:?}", resultat);
println!("taille de () = {} octet", std::mem::size_of::<()>());
bonjour
type unité : ()
taille de () = 0 octet

Dépassement arithmétique#

Proposition 2 (Comportement lors d’un dépassement)

En Rust, le comportement lors d’un dépassement arithmétique (overflow) dépend du mode de compilation :

  • En mode debug (cargo build), le dépassement provoque une panique (panic) à l’exécution.

  • En mode release (cargo build --release), le dépassement effectue un enroulement (wrapping) selon l’arithmétique modulo \(2^n\).

Ce choix de conception permet de détecter les erreurs pendant le développement tout en offrant les performances maximales en production.

Pour un contrôle explicite du comportement en cas de dépassement, Rust fournit quatre familles de méthodes sur les types entiers :

Famille

Comportement

Exemple

checked_*

Retourne Option<T> : None si dépassement

i32::checked_add

wrapping_*

Effectue toujours l’enroulement modulo \(2^n\)

i32::wrapping_add

saturating_*

Plafonne à la valeur minimale ou maximale du type

i32::saturating_add

overflowing_*

Retourne (T, bool) : la valeur enroulée et un indicateur de dépassement

i32::overflowing_add

Exemple 17 (Méthodes de contrôle du dépassement)

Voir le code ci-dessous.

let a: u8 = 250;
let b: u8 = 10;

// checked : retourne None en cas de dépassement
let checked = a.checked_add(b);
println!("checked_add : {:?}", checked);

// wrapping : enroulement modulo 256
let wrapped = a.wrapping_add(b);
println!("wrapping_add : {}", wrapped); // (250 + 10) mod 256 = 4

// saturating : plafonnement à la valeur maximale
let saturated = a.saturating_add(b);
println!("saturating_add : {}", saturated); // 255

// overflowing : valeur enroulée + indicateur booléen
let (val, overflow) = a.overflowing_add(b);
println!("overflowing_add : valeur = {}, dépassement = {}", val, overflow);
checked_add : None
wrapping_add : 4
saturating_add : 255
overflowing_add : valeur = 4, dépassement = true
// Démonstration avec des valeurs signées
let x: i8 = 120;
let y: i8 = 10;

println!("checked : {:?}", x.checked_add(y));       // None (130 > 127)
println!("wrapping : {}", x.wrapping_add(y));        // -126
println!("saturating : {}", x.saturating_add(y));    // 127
let (v, o) = x.overflowing_add(y);
println!("overflowing : valeur = {v}, dépassement = {o}");
checked : None
wrapping : -126
saturating : 127
overflowing : valeur = -126, dépassement = true

Remarque 13

Dans la pratique, le choix de la méthode dépend du contexte. Pour du code où la correction est prioritaire (cryptographie, finance), on préférera checked_* ou saturating_*. Pour des algorithmes de hachage ou de chiffrement, wrapping_* est le choix naturel puisque l’arithmétique modulaire y est intentionnelle.