Structures#
Les structures (structs) sont le principal mécanisme de Rust pour regrouper des données apparentées sous un type nommé. Elles jouent un role analogue aux classes d’autres langages, mais sans héritage : le polymorphisme est assuré par les traits (chapitre 13).
Structures nommées#
Définition 43 (Structure nommée)
Une structure nommée (named struct) regroupe des champs nommés, chacun doté d’un type. Elle se déclare avec le mot-clé struct. Tous les champs doivent etre explicitement initialisés lors de la construction.
struct Point {
x: f64,
y: f64,
}
let p = Point { x: 3.0, y: 4.0 };
println!("({}, {})", p.x, p.y);
(3, 4)
L’accès aux champs se fait par la notation pointée. Lorsque les variables locales portent le meme nom que les champs, Rust autorise un raccourci syntaxique. Par ailleurs, une instance est mutable ou immuable dans son ensemble : le mot-clé mut s’applique a la liaison, pas aux champs individuels.
struct Couleur { r: u8, g: u8, b: u8 }
let r = 255;
let g = 128;
let b = 0;
// Raccourci : équivalent à Couleur { r: r, g: g, b: b }
let orange = Couleur { r, g, b };
println!("RGB({}, {}, {})", orange.r, orange.g, orange.b);
RGB(255, 128, 0)
Remarque 31
Contrairement au C, Rust n’admet pas de valeur par défaut implicite pour les champs. Cette contrainte élimine toute une classe de bogues liés aux données non initialisées.
Syntaxe de mise a jour#
L’opérateur .. copie les champs non spécifiés depuis une instance existante.
Exemple 42 (Syntaxe de mise a jour)
Voir le code ci-dessous.
#[derive(Debug)]
struct Config {
largeur: u32,
hauteur: u32,
plein_ecran: bool,
}
let defaut = Config { largeur: 800, hauteur: 600, plein_ecran: false };
let custom = Config {
largeur: 1920,
hauteur: 1080,
..defaut
};
println!("{:?}", custom);
Config { largeur: 1920, hauteur: 1080, plein_ecran: false }
Remarque 32
La syntaxe .. effectue un déplacement (move) des champs non Copy. Si la source contient un String, ce champ sera déplacé et la source ne sera plus utilisable. Les champs Copy (entiers, flottants, booléens) sont copiés sans affecter la source.
Structures tuples#
Définition 44 (Structure tuple)
Une structure tuple (tuple struct) possède des champs identifiés par leur position, non par un nom. Elle se déclare avec struct, suivi du nom et d’une liste de types entre parenthèses.
struct Coordonnees(f64, f64, f64);
let pos = Coordonnees(1.0, 2.0, 3.0);
println!("x={}, y={}, z={}", pos.0, pos.1, pos.2);
x=1, y=2, z=3
Leur usage principal est le newtype pattern : encapsuler un type existant dans un type distinct.
Exemple 43 (Newtype pattern)
Voir le code ci-dessous.
struct Metres(f64);
struct Secondes(f64);
fn vitesse(distance: Metres, temps: Secondes) -> f64 {
distance.0 / temps.0
}
let d = Metres(100.0);
let t = Secondes(9.58);
println!("Vitesse : {:.2} m/s", vitesse(d, t));
// vitesse(t, d) ne compilerait pas : les types sont distincts
Vitesse : 10.44 m/s
Remarque 33
Le newtype pattern interdit au niveau du système de types les confusions entre grandeurs de meme représentation (mètres et secondes, etc.), sans aucun cout a l’exécution.
Structures unités#
Définition 45 (Structure unité)
Une structure unité (unit struct) ne possède aucun champ, n’occupe aucun espace mémoire, et se déclare par struct Nom;.
struct Marqueur;
let _m = Marqueur;
println!("taille de Marqueur = {} octet", std::mem::size_of::<Marqueur>());
taille de Marqueur = 0 octet
Les structures unités servent principalement a implémenter des traits sans stocker de données.
Blocs impl : méthodes et fonctions associées#
Définition 46 (Bloc impl)
Un bloc impl associe des fonctions a un type. Celles dont le premier paramètre est une forme de self sont des méthodes ; les autres sont des fonctions associées (analogues aux méthodes statiques).
Exemple 44 (Méthodes et fonctions associées)
Voir le code ci-dessous.
struct Rectangle {
largeur: f64,
hauteur: f64,
}
impl Rectangle {
// Fonction associée (constructeur idiomatique)
fn new(largeur: f64, hauteur: f64) -> Self {
Self { largeur, hauteur }
}
// Méthode avec &self : emprunt immuable
fn aire(&self) -> f64 {
self.largeur * self.hauteur
}
// Méthode avec &mut self : emprunt mutable
fn agrandir(&mut self, facteur: f64) {
self.largeur *= facteur;
self.hauteur *= facteur;
}
// Méthode avec self : consommation par valeur
fn en_carre(self) -> Rectangle {
let cote = self.largeur.max(self.hauteur);
Rectangle::new(cote, cote)
}
}
let mut r = Rectangle::new(3.0, 4.0);
println!("Aire = {}", r.aire());
r.agrandir(2.0);
println!("Après agrandissement : {} x {}", r.largeur, r.hauteur);
let carre = r.en_carre();
println!("Carré : {} x {}", carre.largeur, carre.hauteur);
Aire = 12
Après agrandissement : 6 x 8
Carré : 8 x 8
Le paramètre self#
Le premier paramètre d’une méthode détermine comment l’instance est utilisée.
Signature |
Signification |
Usage typique |
|---|---|---|
|
Emprunt immuable |
Lecture, calcul sans modification |
|
Emprunt mutable |
Modification en place |
|
Prise de possession (move) |
Transformation ou consommation de la valeur |
Remarque 34
Le paramètre self est du sucre syntaxique : &self est équivalent a self: &Self, &mut self a self: &mut Self, et self a self: Self. Le type Self (avec une majuscule) désigne le type pour lequel le bloc impl est écrit.
Un type peut posséder plusieurs blocs impl ; le compilateur les fusionne.
Visibilité des champs#
Définition 47 (Visibilité)
Par défaut, les champs d’une structure sont privés : accessibles uniquement dans le module de définition. Le mot-clé pub rend un champ visible depuis l’extérieur. Chaque champ peut avoir sa propre visibilité.
Exemple 45 (Champs publics et privés)
Voir le code ci-dessous.
mod geometrie {
pub struct Cercle {
pub rayon: f64,
centre_x: f64, // privé
centre_y: f64, // privé
}
impl Cercle {
pub fn new(rayon: f64, x: f64, y: f64) -> Self {
Self { rayon, centre_x: x, centre_y: y }
}
pub fn centre(&self) -> (f64, f64) {
(self.centre_x, self.centre_y)
}
}
}
let c = geometrie::Cercle::new(5.0, 1.0, 2.0);
println!("Rayon = {}", c.rayon); // OK : champ public
println!("Centre = {:?}", c.centre()); // accès via méthode publique
// c.centre_x ne compilerait pas : champ privé
Rayon = 5
Centre = (1.0, 2.0)
Lorsqu’une structure possède des champs privés, elle ne peut pas etre instanciée depuis l’extérieur du module. On fournit alors une fonction associée new servant de constructeur — c’est une convention idiomatique en Rust.
Dérivation automatique de traits#
L’attribut #[derive(...)] génère automatiquement l’implémentation de certains traits standard.
Exemple 46 (Traits dérivés courants)
Voir le code ci-dessous.
#[derive(Debug, Clone, PartialEq)]
struct Etudiant {
nom: String,
age: u32,
moyenne: f64,
}
let alice = Etudiant {
nom: String::from("Alice"),
age: 21,
moyenne: 15.5,
};
// Debug : affichage de débogage
println!("{:?}", alice);
// Clone : copie profonde
let alice2 = alice.clone();
// PartialEq : comparaison structurelle
println!("Égaux ? {}", alice == alice2);
Etudiant { nom: "Alice", age: 21, moyenne: 15.5 }
Égaux ? true
Les traits dérivables les plus courants sont : Debug (affichage avec {:?}), Clone (copie profonde), Copy (copie implicite, types simples), PartialEq/Eq (comparaison), PartialOrd/Ord (ordre), Hash (hachage) et Default (valeur par défaut).
#[derive(Debug, Default)]
struct Parametres {
volume: f64,
luminosite: f64,
mode_nuit: bool,
}
let p = Parametres::default();
println!("{:?}", p); // tous les champs à leur valeur par défaut (0.0, 0.0, false)
Parametres { volume: 0.0, luminosite: 0.0, mode_nuit: false }
Structures génériques#
Définition 48 (Structure générique)
Une structure générique déclare des paramètres de type entre chevrons (<T>) après son nom. Ils sont remplacés par des types concrets lors de l’instanciation (monomorphisation).
Exemple 47 (Structure générique)
Voir le code ci-dessous.
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
let entier = Point::new(1, 2);
let flottant = Point::new(1.5, 2.5);
println!("{:?}", entier);
println!("{:?}", flottant);
Point { x: 1, y: 2 }
Point { x: 1.5, y: 2.5 }
On peut utiliser plusieurs paramètres de type. Les blocs impl peuvent restreindre ces paramètres a l’aide de bornes de traits (trait bounds).
#[derive(Debug)]
struct Paire<A, B> { premier: A, second: B }
let p = Paire { premier: "clé", second: 42 };
println!("{:?}", p);
Paire { premier: "clé", second: 42 }
use std::fmt;
#[derive(Debug)]
struct Point<T> { x: T, y: T }
impl<T: fmt::Display> Point<T> {
fn afficher(&self) {
println!("Point({}, {})", self.x, self.y);
}
}
let p = Point { x: 3.14, y: 2.72 };
p.afficher();
Point(3.14, 2.72)
Remarque 35
La généricité est résolue a la compilation par monomorphisation : le compilateur génère une version spécialisée pour chaque type concret, sans cout a l’exécution. Les génériques seront approfondis au chapitre 14.