Chapitre 1 : Notions de base
Section 1 : Sans l'utilisation des génériques
Lorsque l'on définit des fonctions avec une logique identique pour différents types, la'bsence de génériques entraîne une duplication de code excessive. Par exemple :
fn main() {
let val_i8: i8 = obtenir_i8(5i8);
let val_i16: i16 = obtenir_i16(5i16);
let val_i32: i32 = obtenir_i32(5i32);
let val_i64: i64 = obtenir_i64(5i64);
let val_u8: u8 = obtenir_u8(5u8);
let val_u16: u16 = obtenir_u16(5u16);
let val_u32: u32 = obtenir_u32(5u32);
let val_u64: u64 = obtenir_u64(5u64);
}
fn obtenir_i8(val: i8) -> i8 { val }
fn obtenir_i16(val: i16) -> i16 { val }
fn obtenir_i32(val: i32) -> i32 { val }
fn obtenir_i64(val: i64) -> i64 { val }
fn obtenir_u8(val: u8) -> u8 { val }
fn obtenir_u16(val: u16) -> u16 { val }
fn obtenir_u32(val: u32) -> u32 { val }
fn obtenir_u64(val: u64) -> u64 { val }
Section 2 : Introduction aux génériques
Les génériques permettent de simplifier le code en créant des fonctions génériques qui fonctionnnent avec n'importe quel type :
fn main() {
let res_i8: i8 = extraire_valeur(5i8);
let res_i16: i16 = extraire_valeur(5i16);
let res_i32: i32 = extraire_valeur(5i32);
let res_i64: i64 = extraire_valeur(5i64);
let res_u8: u8 = extraire_valeur(5u8);
let res_u16: u16 = extraire_valeur(5u16);
let res_u32: u32 = extraire_valeur(5u32);
let res_u64: u64 = extraire_valeur(5u64);
}
fn extraire_valeur<T>(donnee: T) -> T { donnee }
Section 3 : Avantages et mécanisme
Les génériques sont des abstractions qui remplacent des types concrets, réduisant ainsi la répétition dans le code. En Rust, ils n'entraînent pas de perte de performance car la monomorphisation est appliquée lors de la compilation : le compilateur substitue les génériques par des types spécifiques, générant du code optimisé.
Chapitre 2 : Utilisations courantes
Section 1 : Génériques dans les structures
Les structures peuvent utiliser des génériques pour stocker des données de types variés :
#[derive(Debug)]
struct Coordonnee<T> {
abscisse: T,
ordonnee: T,
}
fn main() {
let point_entier: Coordonnee<i32> = Coordonnee { abscisse: 10, ordonnee: 20 };
println!("{:?}", point_entier);
let point_flottant: Coordonnee<f64> = Coordonnee { abscisse: 1.5, ordonnee: 2.8 };
println!("{:?}", point_flottant);
}
Pour des types différents, on utilise plusieurs paramètres de type :
#[derive(Debug)]
struct Paquet<X, Y> {
premier: X,
second: Y,
}
fn main() {
let mixte: Paquet<u32, &str> = Paquet { premier: 42, second: "test" };
println!("{:?}", mixte);
}
Section 2 : Génériques dans les énumérations
Les énumérations standard comme Option et Result utilisent des génériques. Voici un exemple personnalisé :
#[derive(Debug)]
enum Notification<A, B> {
Texte(u32),
Donnee(A),
Reference(B),
}
fn main() {
let alerte1: Notification<i64, String> = Notification::Texte(1);
let alerte2: Notification<i64, String> = Notification::Donnee(100);
let alerte3: Notification<i64, String> = Notification::Reference("info".to_string());
println!("{:?}", alerte1);
println!("{:?}", alerte2);
println!("{:?}", alerte3);
}
Section 3 : Génériques dans les méthodes
Les méthodes implémentées sur des structures génériques peuvent aussi utiliser des types génériques :
#[derive(Debug)]
struct Vecteur<T> {
composante_x: T,
composante_y: T,
}
impl<T> Vecteur<T> {
fn obtenir_x(&self) -> &T {
&self.composante_x
}
}
fn main() {
let v: Vecteur<i32> = Vecteur { composante_x: 5, composante_y: 10 };
println!("x = {}", v.obtenir_x());
}
Les méthodes peuvent également introduire de nouveaux paramètres de type pour des opérations compelxes :
#[derive(Debug)]
struct Paire<U, V> {
alpha: U,
beta: V,
}
impl<U, V> Paire<U, V> {
fn fusionner<W, X>(self, autre: Paire<W, X>) -> Paire<U, X> {
Paire { alpha: self.alpha, beta: autre.beta }
}
}
fn main() {
let paire1: Paire<i32, f64> = Paire { alpha: 1, beta: 2.5 };
let paire2: Paire<&str, char> = Paire { alpha: "rust", beta: 'R' };
let resultat: Paire<i32, char> = paire1.fusionner(paire2);
println!("alpha = {}, beta = {}", resultat.alpha, resultat.beta);
}