Thanks!
X
Paypal
Github sponsorship
Patreon

Rust

Fork me on GitHub

II. Spécificités de Rust

3. Généricité

Reprenons donc notre précédent exemple :

Runfn affiche_chat(chat: &Chat) -> String {
    println!("{} est un {}", chat.get_nom(), chat.get_espece());
}

fn affiche_chien(chien: &Chien) -> String {
    println!("{} est un {}", chien.get_nom(), chien.get_espece());
}

Comme je vous le disais, avec deux espèces d'animaux, ça ne représente que 2 fonctions, mais ça deviendra très vite long à écrire si on veut en rajouter 40. C'est donc ici qu'intervient la généricité.

La généricité en Rust

Commençons par la base en donnant une description de ce que c'est : "c'est une fonctionnalité qui autorise le polymorphisme paramétrique (ou juste polymorphisme pour aller plus vite)". Pour faire simple, ça permet de manipuler des objets différents du moment qu'ils implémentent le ou les traits requis.

Par exemple, on pourrait manipuler un chien robot, il implémenterait le trait Machine et le trait Animal :

Runtrait Machine {
    fn get_nombre_de_vis(&self) -> u32;
    fn get_numero_de_serie(&self) -> &str;
}

trait Animal {
    fn get_nom(&self) -> &str;
    fn get_nombre_de_pattes(&self) -> u32;
}

struct ChienRobot {
    nom: String,
    nombre_de_pattes: u32,
    numero_de_serie: String,
}

impl Animal for ChienRobot {
    fn get_nom(&self) -> &str {
        &self.nom
    }

    fn get_nombre_de_pattes(&self) -> u32 {
        self.nombre_de_pattes
    }
}

impl Machine for ChienRobot {
    fn get_nombre_de_vis(&self) -> u32 {
        40123
    }

    fn get_numero_de_serie(&self) -> &str {
        &self.numero_de_serie
    }
}

Ainsi, il nous est désormais possible de faire :

Runfn presentation_animal<T: Animal>(animal: T) {
    println!(
        "Il s'appelle {} et il a {} patte()s !",
        animal.get_nom(),
        animal.get_nombre_de_pattes(),
    );
}

let super_chien = ChienRobot {
    nom: "Super chien".to_owned(),
    nombre_de_pattes: 4,
    numero_de_serie: String::from("super chien DZ442"),
};

presentation_animal(super_chien);

Mais comme c'est aussi une machine, on peut aussi faire :

Runfn description_machine<T: Machine>(machine: T) {
    println!(
        "Le modèle {} a {} vis",
        machine.get_numero_de_serie(),
        machine.get_nombre_de_vis(),
    );
}

C'est pas trop génial ?

Revenons-en maintenant à notre problème initial : "comment faire avec 40 espèces d'animaux différentes" ? Je pense que vous commencez à voir où je veux en venir je présume ? Non ? Très bien, dans ce cas prenons un autre exemple :

Runtrait Animal {
    fn get_nom(&self) -> &str {
        &self.nom
    }

    fn get_nombre_de_pattes(&self) -> u32 {
        self.nombre_de_pattes
    }
}

struct Chien {
    nom: String,
    nombre_de_pattes: u32,
}

struct Chat {
    nom: String,
    nombre_de_pattes: u32,
}

struct Oiseau {
    nom: String,
    nombre_de_pattes: u32,
}

struct Araignee {
    nom: String,
    nombre_de_pattes: u32,
}

impl Animal for Chien {}
impl Animal for Chat {}
impl Animal for Oiseau {}
impl Animal for Araignee {}

fn affiche_animal<T: Animal>(animal: T) {
    println!(
        "Cet animal s'appelle {} et il a {} patte(s)",
        animal.get_nom(),
        animal.get_nombre_de_pattes(),
    );
}

let chat = Chat { nom: String::from("Félix"), nombre_de_pattes: 4 };
let spider = Araignee { nom: String::from("Yuuuurk"), nombre_de_pattes: 8 };

affiche_animal(chat);
affiche_animal(spider);

Pas mal hein ? Et pourtant... Ce code ne compile pas !

Pourquoi ?!

Tout simplement parce qu'une implémentation par défaut d'une méthode n'aura accès qu'aux objets fournis par le trait lui-même. Dans le cas présent, self.nom et self.nombre_de_pattes ne sont pas définis dans le trait et ne peuvent pas donc être utilisés. Cependant, si le trait fournissait des méthodes nombre_de_pattes() etnom(), on pourrait les appeler.

Voici un code fonctionnant pour ce cas :

Runstruct Chien {
    nom: String,
    nombre_de_pattes: u32,
}

struct Chat {
    nom: String,
    nombre_de_pattes: u32,
}

trait Animal {
    fn get_nom(&self) -> &str;
    fn get_nombre_de_pattes(&self) -> u32;
    fn affiche(&self) {
        println!("Je suis un animal qui s'appelle {} et j'ai {} pattes !",
                 self.get_nom(),
                 self.get_nombre_de_pattes());
    }
}

// On implémente les méthodes prévues dans le trait Animal, sauf celles par
// défaut.
impl Animal for Chien {
    fn get_nom(&self) -> &str {
        &self.nom
    }

    fn get_nombre_de_pattes(&self) -> u32 {
        self.nombre_de_pattes
    }
}

// On fait de même, mais on a quand même envie de surcharger la méthode par
// défaut...
impl Animal for Chat {
    fn get_nom(&self) -> &str {
        &self.nom
    }

    fn get_nombre_de_pattes(&self) -> u32 {
        self.nombre_de_pattes
    }

    // On peut même 'surcharger' une méthode par défaut dans le trait - il
    // suffit de la réimplémenter
    fn affiche(&self) {
        println!(
            "Je suis un animal - un chat même qui s'appelle {} !",
            self.get_nom(),
        );
    }
}

fn main() {
    fn affiche_animal<T: Animal>(animal: T) {
        animal.affiche();
    }

    let chat = Chat { nom: "Félix".to_owned(), nombre_de_pattes: 4};
    let chien = Chien { nom: "Rufus".to_owned(), nombre_de_pattes: 4};

    affiche_animal(chat);
    affiche_animal(chien);
}

La seule contrainte étant que, même si l'implémentation des méthodes est la même, il faudra, à chaque structure héritant d'un trait, la réimplémenter... Cela dit, les macros pourraient grandement faciliter cette étape répétitive et laborieuse, mais nous verrons cela plus tard.

Where

Il est aussi possible d'écrire un type/une fonction générique en utilisant le mot-clé where :

Runfn affiche_animal<T>(animal: T)
where
    T: Animal
{
    println!(
        "Cet animal s'appelle {} et il a {} patte(s)",
        animal.get_nom(),
        animal.get_nombre_de_pattes(),
    );
}

Dans l'exemple précédent, cela n'apporte strictement rien. Cependant, where permet d'ajouter des clauses à la généricité et est plus lisible sur les fonctions/types prenant beaucoup de paramètres génériques :

Runuse std::fmt::Debug;

trait UnTrait<A, B> {
    fn copy_a(&mut self, a: &A);
    fn clone_b(&mut self, b: &B);
}

struct UneStruct<A, B> {
    a: A,
    b: B,
}

impl<A, B> UnTrait<A, B> for UneStruct<A, B>
where
    A: Copy + Debug,
    B: Clone,
{
    fn copy_a(&mut self, a: &A) {
        self.a = *a;
        println!("copy_a: {:?}", a);
    }

    fn clone_b(&mut self, b: &B) {
        self.b = b.clone();
    }
}