Phase 4Organisation du code

#14 Traits

trait, impl, dyn

Qu'est-ce qu'un trait ?

Un trait définit un ensemble de comportements (méthodes) qu'un type peut implémenter. C'est similaire aux interfaces dans d'autres langages comme Java ou TypeScript.

Les traits permettent le polymorphisme : différents types peuvent partager le même comportement tout en ayant des implémentations différentes.

Définir des traits

On définit un trait avec le mot-clé trait, suivi des signatures des méthodes.

Définir un trait
trait Forme {
    fn aire(&self) -> f64;
    fn perimetre(&self) -> f64;
    fn nom(&self) -> &str;
}

Implémenter des traits

On implémente un trait pour un type avec impl Trait for Type. Chaque méthode du trait doit être définie.

Implémenter un trait
struct Cercle {
    rayon: f64,
}

struct Rectangle {
    largeur: f64,
    hauteur: f64,
}

impl Forme for Cercle {
    fn aire(&self) -> f64 {
        std::f64::consts::PI * self.rayon * self.rayon
    }

    fn perimetre(&self) -> f64 {
        2.0 * std::f64::consts::PI * self.rayon
    }

    fn nom(&self) -> &str {
        "Cercle"
    }
}

impl Forme for Rectangle {
    fn aire(&self) -> f64 {
        self.largeur * self.hauteur
    }

    fn perimetre(&self) -> f64 {
        2.0 * (self.largeur + self.hauteur)
    }

    fn nom(&self) -> &str {
        "Rectangle"
    }
}

fn main() {
    let c = Cercle { rayon: 5.0 };
    let r = Rectangle { largeur: 4.0, hauteur: 6.0 };

    println!("{} : aire = {:.2}", c.nom(), c.aire());
    println!("{} : aire = {}", r.nom(), r.aire());
}

Méthodes par défaut

Un trait peut fournir une implémentation par défaut pour certaines méthodes. Les types qui implémentent le trait peuvent utiliser la version par défaut ou la remplacer.

Méthodes par défaut
trait Resumable {
    fn auteur(&self) -> &str;
    fn contenu(&self) -> &str;

    // Méthode par défaut
    fn resume(&self) -> String {
        format!("{} — {}", self.auteur(), self.contenu())
    }
}

struct Article {
    auteur: String,
    titre: String,
}

impl Resumable for Article {
    fn auteur(&self) -> &str {
        &self.auteur
    }

    fn contenu(&self) -> &str {
        &self.titre
    }

    // Remplacer la méthode par défaut
    fn resume(&self) -> String {
        format!("Article de presse par {}", self.auteur)
    }
}

struct Tweet {
    utilisateur: String,
    texte: String,
}

impl Resumable for Tweet {
    fn auteur(&self) -> &str {
        &self.utilisateur
    }

    fn contenu(&self) -> &str {
        &self.texte
    }

    // Utilise la méthode par défaut de resume()
}

Trait bounds

Les trait bounds permettent de restreindre les types génériques à ceux qui implémentent un trait donné. On peut utiliser la syntaxeimpl Trait ou la notation <T: Trait>.

Trait bounds
// Syntaxe avec impl Trait (simple)
fn afficher_resume(item: &impl Resumable) {
    println!("Résumé : {}", item.resume());
}

// Syntaxe avec trait bound (explicite)
fn afficher_resume_generique<T: Resumable>(item: &T) {
    println!("Résumé : {}", item.resume());
}

// Plusieurs trait bounds avec +
fn afficher_et_debug<T: Resumable + std::fmt::Debug>(item: &T) {
    println!("{:?}", item);
    println!("Résumé : {}", item.resume());
}

// Retourner un type qui implémente un trait
fn creer_tweet() -> impl Resumable {
    Tweet {
        utilisateur: String::from("@rustlang"),
        texte: String::from("Rust, c'est super !"),
    }
}

fn main() {
    let article = Article {
        auteur: String::from("Le Monde"),
        titre: String::from("Rust élu langage préféré"),
    };

    let tweet = creer_tweet();

    afficher_resume(&article);
    afficher_resume(&tweet);
}

À vous de jouer

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

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