II. Spécificités de Rust
4. 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(),
);
}
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);
Et pourtant... Ce code ne compile pas !
C'est parce qu'une implémentation par défaut d'une méthode n'aura accès qu'à ce qui est fourni 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 la réimplémenter pour chaque type implémentant ce trait... Cela dit, les macros pourraient grandement faciliter cette étape répétitive et laborieuse, mais nous verrons cela plus tard.
Combinaisons de traits
Il est possible de demander à ce qu'un type générique implémente plus d'un trait. On peut les combiner en utilisant le signe +
. Cela permettra d'avoir accès aux méthodes fournis par tous les traits qui sont requis :
Run// On implémente `Debug` sur `Cat` avec `#[derive()]`:
#[derive(Debug)]
struct Chat {
nom: String,
nombre_de_pattes: u32,
}
fn affiche_animal<T: Animal + Debug>(animal: T) {
// On utilise `Debug` avec `{:?}`.
println!("Affichage de {:?}", animal);
// On utilise `Animal` avec `.affiche()`.
animal.affiche();
}
fn main() {
let chat = Chat { nom: "Félix".to_owned(), nombre_de_pattes: 4 };
affiche_animal(chat);
}
Dans l'exemple ci-dessus, comme le type Chat
implémente bien les traits Animal
et Debug
, on peut l'utiliser comme argument dans la fonction affiche_animal
.
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 est plus lisible sur les fonctions/types prenant beaucoup de paramètres génériques :
Runfn affiche_2_animaux<T, T2>(animal1: T, animal2: T2)
where
T: Animal + Debug,
T2: Animal + Debug + Clone
{
// ...
}