Phase 4Organisation du code

#15 Génériques

<T>, where, bounds

Fonctions génériques

Les génériques permettent d'écrire du code qui fonctionne avec plusieurs types, sans dupliquer la logique. Au lieu de spécifier un type concret, on utilise un paramètre de type (souvent nommé T).

Fonction générique
// Sans génériques : il faudrait écrire une fonction par type
fn plus_grand_i32(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}

// Avec génériques : une seule fonction pour tous les types comparables
fn plus_grand<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

fn main() {
    let resultat = plus_grand(42, 100);
    println!("Le plus grand : {}", resultat); // 100

    let resultat = plus_grand('a', 'z');
    println!("Le plus grand : {}", resultat); // z
}

Le T: PartialOrd est un trait bound : il dit queT doit implémenter le trait PartialOrd (être comparable).

Structs génériques

Les structs peuvent aussi être génériques. Cela permet de créer des structures de données réutilisables.

Struct générique
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn new(x: T, y: T) -> Self {
        Point { x, y }
    }

    fn x(&self) -> &T {
        &self.x
    }
}

// Implémentation spécifique pour f64
impl Point<f64> {
    fn distance_origine(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let entier = Point::new(5, 10);
    let flottant = Point::new(1.5, 3.7);

    println!("Point entier : ({}, {})", entier.x, entier.y);
    println!("Point flottant : ({}, {})", flottant.x, flottant.y);
    println!("Distance : {:.2}", flottant.distance_origine());
}

Plusieurs paramètres de type

Plusieurs paramètres de type
struct Point<T, U> {
    x: T,
    y: U,
}

impl<T, U> Point<T, U> {
    fn melanger<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Bonjour", y: 'c' };

    let p3 = p1.melanger(p2);
    println!("Point mixte : ({}, {})", p3.x, p3.y);
    // Point mixte : (5, c)
}

Enums génériques

Vous utilisez déjà des enums génériques sans le savoir ! Option<T> et Result<T, E> sont des enums génériques de la bibliothèque standard.

Enum générique personnalisé
// Option et Result sont définis comme ça :
// enum Option<T> {
//     Some(T),
//     None,
// }
//
// enum Result<T, E> {
//     Ok(T),
//     Err(E),
// }

// Votre propre enum générique
enum Reponse<T> {
    Succes(T),
    Erreur(String),
    EnCours,
}

fn charger_donnees(id: u32) -> Reponse<String> {
    if id == 1 {
        Reponse::Succes(String::from("Données trouvées"))
    } else if id == 0 {
        Reponse::EnCours
    } else {
        Reponse::Erreur(String::from("ID invalide"))
    }
}

fn main() {
    match charger_donnees(1) {
        Reponse::Succes(data) => println!("OK : {}", data),
        Reponse::Erreur(msg) => println!("Erreur : {}", msg),
        Reponse::EnCours => println!("Chargement..."),
    }
}

Trait bounds avec génériques

Les trait bounds contraignent les types génériques à implémenter certains traits. On peut combiner plusieurs bounds avec +.

Trait bounds
use std::fmt::Display;

// T doit implémenter Display ET PartialOrd
fn afficher_plus_grand<T: Display + PartialOrd>(a: T, b: T) {
    let resultat = if a > b { a } else { b };
    println!("Le plus grand est : {}", resultat);
}

// Avec la syntaxe impl Trait
fn doubler_et_afficher(valeur: &(impl Display + Clone)) {
    let copie = valeur.clone();
    println!("Original : {}, Copie : {}", valeur, copie);
}

fn main() {
    afficher_plus_grand(42, 100);
    afficher_plus_grand("alpha", "beta");
    doubler_et_afficher(&String::from("Bonjour"));
}

La clause where

Quand les trait bounds deviennent complexes, la clause where rend le code plus lisible en déplaçant les contraintes après la signature.

La clause where
use std::fmt::{Debug, Display};

// Sans where (difficile à lire)
fn traiter<T: Display + Clone + Debug, U: Clone + Debug>(t: &T, u: &U) {
    println!("{:?} {:?}", t, u);
}

// Avec where (beaucoup plus clair)
fn traiter_propre<T, U>(t: &T, u: &U)
where
    T: Display + Clone + Debug,
    U: Clone + Debug,
{
    println!("{:?} {:?}", t, u);
}

// Utile pour les retours conditionnels
fn creer_affichable<T>(valeur: T) -> impl Display
where
    T: Display + Clone,
{
    valeur.clone()
}

fn main() {
    traiter_propre(&42, &"Bonjour");
    println!("{}", creer_affichable("Rust"));
}

À vous de jouer

Essayez les commandes ci-dessous pour compiler et exécuter le programme :

terminal — cargo
user@stemlegacy:~/generics-demo$