Rust

Any donation is very welcome
Fork me on GitHub

II. Spécificités de Rust

3. Les traits

Commençons par donner une rapide définition : un trait est un ensemble de méthodes que l'objet sur lequel il est appliqué doit implémenter.

Dans le chapitre précédent, il nous fallait implémenter la méthode drop pour pouvoir implémenter le trait Drop. Et au cas où vous ne vous en doutiez pas, sachez que les traits sont utilisés partout en Rust. On en retrouve même sur de simples types comme les i32 ou les f64 !

On va prendre un exemple tout simple : additionner deux f64. La doc nous dit ici que le trait Add a été implémenté sur le type f64. Ce qui nous permet de faire :

Runlet valeur = 1f64;

println!("{}", valeur + 3f64);

Add était un trait implémenté "par défaut". Si ce n'est pas le cas, vous devez importer un trait pour utiliser les fonctions qui y sont associées. Exemple :

Runuse std::str::FromStr;

println!("{}", f64::from_str("3.6").unwrap());

Facile n'est-ce pas ? Les traits fournis par la bibliothèque standard et implémentés sur les types standards apportent beaucoup de fonctionnalités. Si jamais vous avez besoin de quelque chose, il y a de fortes chances que ça existe déjà. À vous de chercher.

Je vous ai montré comment importer et utiliser un trait, maintenant il est temps de voir comment en créer un !

Créer un trait

C'est relativement similaire à la création d'une structure :

Runtrait Animal {
    fn get_espece(&self) -> &str;
}

Facile, n'est-ce pas ? Maintenant un petit exemple :

Runtrait Animal {
    fn get_espece(&self) -> &str;
    fn get_nom(&self) -> &str;
}

struct Chien {
    nom: String,
}

impl Animal for Chien {
    fn get_espece(&self) -> &str {
        "Chien"
    }

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

struct Chat {
    nom: String,
}

impl Animal for Chat {
    fn get_espece(&self) -> &str {
        "Chat"
    }

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

let chat = Chat { nom: String::from("Fifi") };
let chien = Chien { nom: String::from("Loulou") };

println!("{} est un {}", chat.get_nom(), chat.get_espece());
println!("{} est un {}", chien.get_nom(), chien.get_espece());

Je tiens à vous rappeler qu'il est tout à fait possible d'implémenter un trait disponible dans la bibliothèque standard comme je l'ai fait avec le trait Drop.

Il est aussi possible d'écrire une implémentation "par défaut" de la méthode directement dans le trait. Ça permet d'éviter d'avoir à réécrire la méthode pour chaque objet sur lequel le trait est implémenté. Exemple :

Runtrait Animal {
    fn get_espece(&self) -> &str;

    fn presentation(&self) -> String {
        format!("Je suis un {} !", self.get_espece())
    }
}

impl Animal for Chat {
    pub fn get_espece(&self) -> &str {
        "Chat"
    }
}

Ici, je ne définis que la méthode get_espece car presentation fait déjà ce que je veux.

Vous n'en voyez peut-être pas encore l'intérêt mais sachez cependant que c'est vraiment très utile. Quoi de mieux qu'un autre exemple pour vous le prouver ? :D

Runfn afficher_infos<T: Animal>(animal: &T) {
    println!("{} est un {}", animal.get_nom(), animal.get_espece());
}

"C'est quoi ce <T: Animal> ?!"

Pour ceux qui ont fait du C++ ou du Java, c'est relativement proche des templates. Pour les autres, sachez juste que les templates ont été inventés pour permettre de faire du polymorphisme. Un exemple (encore un !) :

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

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

Dans le cas présent, ça va, cela ne représente que deux fonctions. Maintenant si on veut ajouter 40 autres espèces d'animaux, on devrait écrire une fonction pour chacune ! Pas très pratique... Utiliser la généricité est donc la meilleure solution. Et c'est ce dont il sera question dans le prochain chapitre !

Aller plus loin

J'en profite maintenant pour vous montrer quelques utilisation de traits comme Range (que l'on avait déjà rapidement abordé dans le chapitre des boucles). Ce dernier peut vous permettre de faire :

Runlet s = "hello";

println!("{}", s);
println!("{}", &s[0..2]);
println!("{}", &s[..3]);
println!("{}", &s[3..]);

Ce qui donnera :

hello
he
hel
lo

Cela fonctionne aussi sur les slices :

Runlet v: &[u8] = &[0; 10]; // on crée un slice contenant 10 '\0'

println!("{:?}", &v[0..2]);
println!("{:?}", &v[..3]);
println!("{:?}", &v[3..]);

Ce qui donne :

[0, 0]
[0, 0, 0]
[0, 0, 0, 0, 0, 0, 0]

Voilà qui devrait vous donner un petit aperçu de tout ce qu'il est possible de faire avec les traits. Il est maintenant temps de parler de la généricité.