Définir des comportements partagés avec les traits
Un trait décrit une fonctionnalité qu'a un type particulier et qu'il peut partager avec d'autres types, à destination du compilateur Rust. Nous pouvons utiliser les traits pour définir un comportement partagé de manière abstraite. Nous pouvons lier ces traits à un type générique pour exprimer le fait qu'il puisse être de n'importe quel type à condition qu'il ait un comportement donné.
Remarque : les traits sont similaires à ce qu'on appelle parfois les interfaces dans d'autres langages, malgré quelques différences.
Définir un trait
Le comportement d'un type s'exprime via les méthodes que nous pouvons appeler sur ce type. Différents types peuvent partager le même comportement si nous pouvons appeler les mêmes méthodes sur tous ces types. Définir un trait est une manière de regrouper des signatures de méthodes pour définir un comportement nécessaire pour accomplir un objectif.
Par exemple, imaginons que nous avons plusieurs structures qui stockent
différents types et quantités de texte : une structure ArticleDePresse
, qui
contient un reportage dans un endroit donné et un Tweet
qui peut avoir jusqu'à
280 caractères maximum et des métadonnées qui indiquent si cela est un nouveau
tweet, un retweet, ou une réponse à un autre tweet.
Nous voulons construire une crate de bibliothèque agregateur
pour des
agrégateurs de médias qui peut afficher le résumé des données stockées dans une
instance de ArticleDePresse
ou de Tweet
. Pour cela, il nous faut un résumé
pour chaque type, et nous allons demander ce résumé en appelant la méthode
resumer
sur une instance. L'encart 10-12 nous montre la définition d'un trait
public Resumable
qui décrit ce comportement.
Fichier : src/lib.rs
pub trait Resumable {
fn resumer(&self) -> String;
}
Ici, nous déclarons un trait en utilisant le mot-clé trait
et ensuite le nom
du trait, qui est Resumable
dans notre cas. Nous avons aussi déclaré le trait
comme pub
afin que les crates qui dépendent de cette crate puissent
aussi utiliser ce trait, comme nous allons le voir dans quelques exemples.
Entre les accolades, nous déclarons la signature de la méthode qui décrit le
comportement des types qui implémentent ce trait, qui est dans notre cas
fn resumer(&self) -> String
.
A la fin de la signature de la méthode, au lieu de renseigner une implémentation
entre des accolades, nous utilisons un point-virgule. Chaque type qui implémente
ce trait doit renseigner son propre comportement dans le corps de la méthode. Le
compilateur va s'assurer que tous les types qui ont le trait Resumable
auront
la méthode resumer
définie avec cette signature précise.
Un trait peut avoir plusieurs méthodes dans son corps : les signatures des méthodes sont ajoutées ligne par ligne et chaque ligne se termine avec un point-virgule.
Implémenter un trait sur un type
Maintenant que nous avons défini les signatures souhaitées des méthodes du
trait Resumable
, nous pouvons maintenant l'implémenter sur les types de notre
agrégateur de médias. L'encart 10-13 montre une implémentation du trait
Resumable
sur la structure ArticleDePresse
qui utilise le titre, le nom de
l'auteur et le lieu pour créer la valeur de retour de resumer
. Pour la
structure Tweet
, nous définissons resumer
avec le nom d'utilisateur suivi
par le texte entier du tweet, en supposant que le contenu du tweet est déjà
limité à 280 caractères.
Fichier : src/lib.rs
pub trait Resumable {
fn resumer(&self) -> String;
}
pub struct ArticleDePresse {
pub titre: String,
pub lieu: String,
pub auteur: String,
pub contenu: String,
}
impl Resumable for ArticleDePresse {
fn resumer(&self) -> String {
format!("{}, par {} ({})", self.titre, self.auteur, self.lieu)
}
}
pub struct Tweet {
pub nom_utilisateur: String,
pub contenu: String,
pub reponse: bool,
pub retweet: bool,
}
impl Resumable for Tweet {
fn resumer(&self) -> String {
format!("{} : {}", self.nom_utilisateur, self.contenu)
}
}
L'implémentation d'un trait sur un type est similaire à l'implémentation d'une
méthode classique. La différence est que nous ajoutons le nom du trait que nous
voulons implémenter après le impl
, et que nous utilisons ensuite le mot-clé
for
suivi du nom du type sur lequel nous souhaitons implémenter le trait. À
l'intérieur du bloc impl
, nous ajoutons les signatures des méthodes présentes
dans la définition du trait. Au lieu d'ajouter un point-virgule après chaque
signature, nous plaçons les accolades et on remplit le corps de la méthode avec
le comportement spécifique que nous voulons que les méthodes du trait suivent
pour le type en question.
Maintenant que la bibliothèque a implémenté le trait Resumable
sur
ArticleDePresse
et Tweet
, les utilisateurs de cette crate peuvent appeler
les méthodes de l'instance de ArticleDePresse
et Tweet
comme si elles
étaient des méthodes classiques. La seule différence est que le trait ainsi que
les types doivent être introduits dans la portée pour obtenir les méthodes de
trait additionnelles. Voici un exemple de comment la crate binaire pourra
utiliser notre crate de bibliothèque agregateur
:
use agregateur::{Resumable, Tweet};
fn main() {
let tweet = Tweet {
nom_utilisateur: String::from("jean"),
contenu: String::from("Bien sûr, les amis, comme vous le savez probablement déjà"),
reponse: false,
retweet: false,
};
println!("1 nouveau tweet : {}", tweet.resumer());
}
Ce code affichera 1 nouveau tweet : jean : Bien sûr, les amis, comme vous le savez probablement déjà
.
Les autres crates qui dépendent de la crate agregateur
peuvent aussi importer
dans la portée le trait Resumable
afin d'implémenter le trait sur leurs
propres types. Il y a une limitation à souligner avec l'implémentation des
traits, c'est que nous ne pouvons implémenter un trait sur un type qu'à
condition qu'au moins le trait ou le type soit défini localement dans notre
crate. Par exemple, nous pouvons implémenter des traits de la bibliothèque
standard comme Display
sur un type personnalisé comme Tweet
comme une
fonctionnalité de notre crate agregateur
, car le type Tweet
est défini
localement dans notre crate agregateur
. Nous pouvons aussi implémenter
Resumable
sur Vec<T>
dans notre crate agregateur
, car le trait
Resumable
est défini localement dans notre crate agregateur
.
Mais nous ne pouvons pas implémenter des traits externes sur des types externes.
Par exemple, nous ne pouvons pas implémenter le trait Display
sur Vec<T>
à
l'intérieur de notre crate agregateur
, car Display
et Vec<T>
sont définis
dans la bibliothèque standard et ne sont donc pas définis localement dans notre
crate agregateur
. Cette limitation fait partie d'une propriété des programmes
que l'on appelle la cohérence, et plus précisément la règle de l'orphelin,
qui s'appelle ainsi car le type parent n'est pas présent. Cette règle s'assure
que le code des autres personnes ne casse pas votre code et réciproquement.
Sans cette règle, deux crates pourraient implémenter le même trait sur le même
type, et Rust ne saurait pas quelle implémentation utiliser.
Implémentations par défaut
Il est parfois utile d'avoir un comportement par défaut pour toutes ou une partie des méthodes d'un trait plutôt que de demander l'implémentation de toutes les méthodes sur chaque type. Ainsi, si nous implémentons le trait sur un type particulier, nous pouvons garder ou réécrire le comportement par défaut de chaque méthode.
L'encart 10-14 nous montre comment préciser une String par défaut pour la
méthode resumer
du trait Resumable
plutôt que de définir uniquement la
signature de la méthode, comme nous l'avons fait dans l'encart 10-12.
Fichier : src/lib.rs
pub trait Resumable {
fn resumer(&self) -> String {
String::from("(En savoir plus ...)")
}
}
pub struct ArticleDePresse {
pub titre: String,
pub lieu: String,
pub auteur: String,
pub contenu: String,
}
impl Resumable for ArticleDePresse {}
pub struct Tweet {
pub nom_utilisateur: String,
pub contenu: String,
pub reponse: bool,
pub retweet: bool,
}
impl Resumable for Tweet {
fn resumer(&self) -> String {
format!("{}: {}", self.nom_utilisateur, self.contenu)
}
}
Pour utiliser l'implémentation par défaut pour résumer des instances de
ArticleDePresse
au lieu de préciser une implémentation personnalisée, nous
précisons un bloc impl
vide avec impl Resumable for ArticleDePresse {}
.
Même si nous ne définissons plus directement la méthode resumer
sur
ArticleDePresse
, nous avons fourni une implémentation par défaut et précisé
que ArticleDePresse
implémente le trait Resumable
. Par conséquent, nous
pouvons toujours appeler la méthode resumer
sur une instance de
ArticleDePresse
, comme ceci :
use chapter10::{self, ArticleDePresse, Resumable};
fn main() {
let article = ArticleDePresse {
titre: String::from("Les Penguins ont remporté la Coupe Stanley !"),
lieu: String::from("Pittsburgh, PA, USA"),
auteur: String::from("Iceburgh"),
contenu: String::from(
"Les Penguins de Pittsburgh sont une nouvelle fois la meilleure \
équipe de hockey de la LNH.",
),
};
println!("Nouvel article disponible ! {}", article.resumer());
}
Ce code va afficher Nouvel article disponible ! (En savoir plus ...)
.
La création d'une implémentation par défaut pour resumer
n'a pas besoin que
nous modifiions quelque chose dans l'implémentation de Resumable
sur Tweet
dans l'encart 10-13. C'est parce que la syntaxe pour réécrire l'implémentation
par défaut est la même que la syntaxe pour implémenter une méthode qui n'a pas
d'implémentation par défaut.
Les implémentations par défaut peuvent appeler d'autres méthodes du même trait,
même si ces autres méthodes n'ont pas d'implémentation par défaut. Ainsi, un
trait peut fournir de nombreuses fonctionnalités utiles et n'exiger du
développeur qui l'utilise que d'en implémenter une petite partie. Par exemple,
nous pouvons définir le trait Resumable
comme ayant une méthode
resumer_auteur
dont l'implémentation est nécessaire, et ensuite définir une
méthode resumer
qui a une implémentation par défaut qui appelle la méthode
resumer_auteur
:
pub trait Resumable {
fn resumer_auteur(&self) -> String;
fn resumer(&self) -> String {
format!("(Lire plus d'éléments de {} ...)", self.resumer_auteur())
}
}
pub struct Tweet {
pub nom_utilisateur: String,
pub contenu: String,
pub reponse: bool,
pub retweet: bool,
}
impl Resumable for Tweet {
fn resumer_auteur(&self) -> String {
format!("@{}", self.nom_utilisateur)
}
}
Pour pouvoir utiliser cette version de Resumable
, nous avons seulement besoin
de définir resumer_auteur
lorsqu'on implémente le trait sur le type :
pub trait Resumable {
fn resumer_auteur(&self) -> String;
fn resumer(&self) -> String {
format!("(Lire plus d'éléments de {} ...)", self.resumer_auteur())
}
}
pub struct Tweet {
pub nom_utilisateur: String,
pub contenu: String,
pub reponse: bool,
pub retweet: bool,
}
impl Resumable for Tweet {
fn resumer_auteur(&self) -> String {
format!("@{}", self.nom_utilisateur)
}
}
Après avoir défini resumer_auteur
, nous pouvons appeler resumer
sur des
instances de la structure Tweet
, et l'implémentation par défaut de resumer
va appeler resumer_auteur
, que nous avons défini. Comme nous avons implémenté
resumer_auteur
, le trait Resumable
nous a donné le comportement de la
méthode resumer
sans nous obliger à écrire une ligne de code supplémentaire.
use chapter10::{self, Resumable, Tweet};
fn main() {
let tweet = Tweet {
nom_utilisateur: String::from("jean"),
contenu: String::from("Bien sûr, les amis, comme vous le savez probablement déjà"),
reponse: false,
retweet: false,
};
println!("1 nouveau tweet : {}", tweet.resumer());
}
Ce code affichera 1 nouveau tweet : (Lire plus d'éléments de @jean ...)
.
Notez qu'il n'est pas possible d'appeler l'implémentation par défaut à partir d'une réécriture de cette même méthode.
Des traits en paramètres
Maintenant que vous savez comment définir et implémenter les traits, nous pouvons regarder comment utiliser les traits pour définir des fonctions qui acceptent plusieurs types différents.
Par exemple, dans l'encart 10-13, nous avons implémenté le trait Resumable
sur les types ArticleDePresse
et Tweet
. Nous pouvons définir une fonction
notifier
qui va appeler la méthode resumer
sur son paramètre element
, qui
est d'un type qui implémente le trait Resumable
. Pour faire ceci, nous
pouvons utiliser la syntaxe impl Trait
, comme ceci :
pub trait Resumable {
fn resumer(&self) -> String;
}
pub struct ArticleDePresse {
pub titre: String,
pub lieu: String,
pub auteur: String,
pub contenu: String,
}
impl Resumable for ArticleDePresse {
fn resumer(&self) -> String {
format!("{}, par {} ({})", self.titre, self.auteur, self.lieu)
}
}
pub struct Tweet {
pub nom_utilisateur: String,
pub contenu: String,
pub reponse: bool,
pub retweet: bool,
}
impl Resumable for Tweet {
fn resumer(&self) -> String {
format!("{} : {}", self.nom_utilisateur, self.contenu)
}
}
pub fn notifier(element: &impl Resumable) {
println!("Flash info ! {}", element.resumer());
}
Au lieu d'un type concret pour le paramètre element
, nous précisons le mot-clé
impl
et le nom du trait. Ce paramètre accepte n'importe quel type qui
implémente le trait spécifié. Dans le corps de notifier
, nous pouvons appeler
toutes les méthodes sur element
qui proviennent du trait Resumable
, comme
resumer
. Nous pouvons appeler notifier
et passer une instance de
ArticleDePresse
ou de Tweet
. Le code qui appellera la fonction avec un autre
type, comme une String
ou un i32
, ne va pas se compiler car ces types
n'implémentent pas Resumable
.
La syntaxe du trait lié
La syntaxe impl Trait
fonctionne bien pour des cas simples, mais est en
réalité du sucre syntaxique pour une forme plus longue, qui s'appelle le
trait lié, qui ressemble à ceci :
pub fn notifier<T: Resumable>(element: &T) {
println!("Flash info ! {}", element.resumer());
}
Cette forme plus longue est équivalente à l'exemple dans la section précédente, mais est plus verbeuse. Nous plaçons les traits liés dans la déclaration des paramètres de type génériques après un deux-point entre des chevrons.
La syntaxe impl Trait
est pratique pour rendre du code plus concis dans des
cas simples. La syntaxe du trait lié exprime plus de complexité dans certains
cas. Par exemple, nous pouvons avoir deux paramètres qui implémentent
Resumable
. En utilisant la syntaxe impl Trait
, nous aurons ceci :
pub fn notifier(element1: &impl Resumable, element2: &impl Resumable) {
Si nous souhaitons permettre à element1
et element2
d'avoir des types
différents, l'utilisation de impl Trait
est appropriée (du moment que chacun
de ces types implémentent Resumable
). Mais si nous souhaitons forcer les deux
paramètres à être du même type, cela n'est possible à exprimer qu'avec un trait
lié, comme ceci :
pub fn notifier<T: Resumable>(element1: &T, element2: &T) {
Le type générique T
renseigné comme type des paramètres element1
et
element2
contraint la fonction de manière à ce que les types concrets des
valeurs passées en arguments pour element1
et element2
soient identiques.
Renseigner plusieurs traits liés avec la syntaxe +
Nous pouvons aussi préciser que nous attendons plus d'un trait lié. Imaginons
que nous souhaitons que notifier
utilise le formatage d'affichage sur
element
ainsi que la méthode resumer
: nous indiquons dans la définition de
notify
que element
doit implémenter à la fois Display
et Resumable
.
Nous pouvons faire ceci avec la syntaxe +
:
pub fn notifier(element: &(impl Resumable + Display)) {
La syntaxe +
fonctionne aussi avec les traits liés sur des types génériques :
pub fn notifier<T: Resumable + Display>(element: &T) {
Avec les deux traits liés renseignés, le corps de notifier
va appeler
resumer
et utiliser {}
pour formater element
.
Des traits liés plus clairs avec la clause where
L'utilisation de trop nombreux traits liés a aussi ses désavantages. Chaque
type générique a ses propres traits liés, donc les fonctions avec plusieurs
paramètres de type génériques peuvent aussi avoir de nombreuses informations de
traits liés entre le nom de la fonction et la liste de ses paramètres, ce qui
rend la signature de la fonction difficile à lire. Pour cette raison, Rust a une
syntaxe alternative pour renseigner les traits liés, dans une clause where
après la signature de la fonction. Donc, au lieu d'écrire ceci ...
fn une_fonction<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
... nous pouvons utiliser la clause where
, comme ceci :
fn une_fonction<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
La signature de cette fonction est moins encombrée : le nom de la fonction, la liste des paramètres et le type de retour sont plus proches les uns des autres, comme une fonction sans traits liés.
Retourner des types qui implémentent des traits
Nous pouvons aussi utiliser la syntaxe impl Trait
à la place du type de
retour afin de retourner une valeur d'un type qui implémente un trait, comme
ci-dessous :
pub trait Resumable {
fn resumer(&self) -> String;
}
pub struct ArticleDePresse {
pub titre: String,
pub lieu: String,
pub auteur: String,
pub contenu: String,
}
impl Resumable for ArticleDePresse {
fn resumer(&self) -> String {
format!("{}, par {} ({})", self.titre, self.auteur, self.lieu)
}
}
pub struct Tweet {
pub nom_utilisateur: String,
pub contenu: String,
pub reponse: bool,
pub retweet: bool,
}
impl Resumable for Tweet {
fn resumer(&self) -> String {
format!("{} : {}", self.nom_utilisateur, self.contenu)
}
}
fn retourne_resumable() -> impl Resumable {
Tweet {
nom_utilisateur: String::from("jean"),
contenu: String::from("Bien sûr, les amis, comme vous le savez probablement déjà"),
reponse: false,
retweet: false,
}
}
En utilisant impl Resumable
pour le type de retour, nous indiquons que la
fonction retourne_resumable
retourne un type qui implémente le trait
Resumable
sans avoir à écrire le nom du type concret. Dans notre cas,
retourne_resumable
retourne un Tweet
, mais le code qui appellera cette
fonction ne le saura pas.
La capacité de retourner un type qui est uniquement caractérisé par le trait
qu'il implémente est tout particulièrement utile dans le cas des fermetures et
des itérateurs, que nous verrons au chapitre 13. Les fermetures et les
itérateurs créent des types que seul le compilateur est en mesure de comprendre
ou alors des types qui sont très longs à définir. La syntaxe impl Trait
vous
permet de renseigner de manière concise qu'une fonction retourne un type
particulier qui implémente le trait Iterator
sans avoir à écrire un très long
type.
Cependant, vous pouvez seulement utiliser impl Trait
si vous retournez un
seul type possible. Par exemple, ce code va retourner soit un
ArticleDePresse
, soit un Tweet
, alors que le type de retour avec
impl Resumable
ne va pas fonctionner :
pub trait Resumable {
fn resumer(&self) -> String;
}
pub struct ArticleDePresse {
pub titre: String,
pub lieu: String,
pub auteur: String,
pub contenu: String,
}
impl Resumable for ArticleDePresse {
fn resumer(&self) -> String {
format!("{}, par {} ({})", self.titre, self.auteur, self.lieu)
}
}
pub struct Tweet {
pub nom_utilisateur: String,
pub contenu: String,
pub reponse: bool,
pub retweet: bool,
}
impl Resumable for Tweet {
fn resumer(&self) -> String {
format!("{} : {}", self.nom_utilisateur, self.contenu)
}
}
fn retourne_resumable(estArticle: bool) -> impl Resumable {
if estArticle {
ArticleDePresse {
titre: String::from("Les Penguins ont remporté la Coupe Stanley !"),
lieu: String::from("Pittsburgh, PA, USA"),
auteur: String::from("Iceburgh"),
contenu: String::from("Les Penguins de Pittsburgh sont une nouvelle fois la \
meilleure équipe de hockey de la LNH."),
}
} else {
Tweet {
nom_utilisateur: String::from("jean"),
contenu: String::from("Bien sûr, les amis, comme vous le savez probablement déjà"),
reponse: false,
retweet: false,
}
}
}
Retourner soit un ArticleDePresse
, soit un Tweet
n'est pas autorisé à cause
des restrictions sur la façon dont la syntaxe impl Trait
est implémentée dans
le compilateur. Nous verrons comment écrire une fonction avec ce comportement
dans une section du
chapitre 17.
Corriger la fonction le_plus_grand
avec les traits liés
Maintenant que vous savez comment renseigner le comportement que vous souhaitez
utiliser en utilisant les traits liés des paramètres de type génériques,
retournons à l'encart 10-5 pour corriger la définition de la fonction
le_plus_grand
qui utilise un paramètre de type générique ! La dernière fois
que nous avons essayé de lancer ce code, nous avions l'erreur suivante :
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src/main.rs:5:17
|
5 | if element > le_plus_grand {
| ------- ^ ------------- T
| |
| T
|
help: consider restricting type parameter `T`
|
1 | fn le_plus_grand<T: std::cmp::PartialOrd>(liste: &[T]) -> T {
| ++++++++++++++++++++++
For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` due to previous error
Dans le corps de le_plus_grand
, nous voulions comparer les deux valeurs du
type T
en utilisant l'opérateur plus grand que (>
). Comme cet opérateur
est défini comme une méthode par défaut dans le trait de la bibliothèque
standard std::cmp::PartialOrd
, nous devons préciser PartialOrd
dans les
traits liés pour T
afin que la fonction le_plus_grand
puisse fonctionner
sur les slices de n'importe quel type que nous pouvons comparer. Nous n'avons
pas besoin d'importer PartialOrd
dans la portée car il est importé dans
l'étape préliminaire. Changez la signature de le_plus_grand
par quelque chose
comme ceci :
fn le_plus_grand<T: PartialOrd>(liste: &[T]) -> T {
let mut le_plus_grand = liste[0];
for &element in liste {
if element > le_plus_grand {
le_plus_grand = element;
}
}
le_plus_grand
}
fn main() {
let liste_de_nombres = vec![34, 50, 25, 100, 65];
let resultat = le_plus_grand(&liste_de_nombres);
println!("Le plus grand nombre est {}", resultat);
let liste_de_caracteres = vec!['y', 'm', 'a', 'q'];
let resultat = le_plus_grand(&liste_de_caracteres);
println!("Le plus grand caractère est {}", resultat);
}
Cette fois, lorsque nous allons compiler le code, nous aurons un ensemble d'erreurs différent :
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0508]: cannot move out of type `[T]`, a non-copy slice
-- > src/main.rs:2:23
|
2 | let mut le_plus_grand = liste[0];
| ^^^^^^^^
| |
| cannot move out of here
| move occurs because `liste[_]` has type `T`, which does not implement the `Copy` trait
| help: consider borrowing here: `&liste[0]`
error[E0507]: cannot move out of a shared reference
-- > src/main.rs:4:18
|
4 | for &element in liste {
| -------- ^^^^^
| ||
| |data moved here
| |move occurs because `element` has type `T`, which does not implement the `Copy` trait
| help: consider removing the `&`: `element`
Some errors have detailed explanations: E0507, E0508.
For more information about an error, try `rustc --explain E0507`.
error: could not compile `chapter10` due to 2 previous errors
L'élement-clé dans ces erreurs est cannot move out of type [T], a non-copy slice
(impossible de déplacer une valeur hors du type [T]
, slice non
Copy
). Avec notre version non générique de la fonction le_plus_grand
, nous
avions essayé de trouver le plus grand i32
ou char
. Comme nous l'avons vu
dans la section “Données uniquement sur la pile : la
copie” du chapitre 4, les types comme
i32
et char
ont une taille connue et peuvent être stockés sur la pile, donc
ils implémentent le trait Copy
. Mais quand nous avons rendu générique la
fonction le_plus_grand
, il est devenu possible que le paramètre liste
contienne des types qui n'implémentent pas le trait Copy
. Par conséquent,
nous ne pouvons pas forcément déplacer la valeur de list[0]
dans notre
variable le_plus_grand
, ce qui engendre cette erreur.
Pour pouvoir appeler ce code avec seulement les types qui implémentent le trait
Copy
, nous pouvons ajouter Copy
aux traits liés de T
! L'encart 10-15 nous
montre le code complet d'une fonction générique le_plus_grand
qui va se
compiler tant que le type des valeurs dans la slice que nous passons dans la
fonction implémente les traits PartialOrd
et Copy
, comme le font i32
et
char
.
Fichier : src/main.rs
fn le_plus_grand<T: PartialOrd + Copy>(liste: &[T]) -> T { let mut le_plus_grand = liste[0]; for &element in liste { if element > le_plus_grand { le_plus_grand = element; } } le_plus_grand } fn main() { let liste_de_nombres = vec![34, 50, 25, 100, 65]; let resultat = le_plus_grand(&liste_de_nombres); println!("Le nombre le plus grand est {}", resultat); let liste_de_caracteres = vec!['y', 'm', 'a', 'q']; let resultat = le_plus_grand(&liste_de_caracteres); println!("Le plus grand caractère est {}", resultat); }
Si nous ne souhaitons pas restreindre la fonction le_plus_grand
aux types qui
implémentent le trait Copy
, nous pouvons préciser que T
a le trait lié
Clone
plutôt que Copy
. Ainsi, nous pouvons cloner chaque valeur dans la
slice lorsque nous souhaitons que la fonction le_plus_grand
en prenne
possession. L'utilisation de la fonction clone
signifie que nous allons
potentiellement allouer plus d'espace sur le tas dans le cas des types qui
possèdent des données sur le tas, comme String
, et les allocations sur le tas
peuvent être lentes si nous travaillons avec des grandes quantités de données.
Une autre façon d'implémenter le_plus_grand
est de faire en sorte que la
fonction retourne une référence à une valeur T
de la slice. Si nous changeons
le type de retour en &T
à la place de T
et que nous adaptons le corps de la
fonction afin de retourner une référence, nous n'aurions alors plus besoin des
traits liés Clone
ou Copy
et nous pourrions ainsi éviter l'allocation sur
le tas. Essayez d'implémenter ces solutions alternatives par vous-même !
Si vous bloquez sur des erreurs à propos des durées de vie (lifetimes), lisez
la suite : la section suivante, “La conformité des références avec les durées
de vies” vous expliquera cela, mais les durées de vie ne sont pas nécessaires
pour résoudre ces exercices.
Utiliser les traits liés pour conditionner l'implémentation des méthodes
En utilisant un trait lié avec un bloc impl
qui utilise les paramètres de type
génériques, nous pouvons implémenter des méthodes en fonction des types qui
implémentent des traits particuliers. Par exemple, le type Paire<T>
de
l'encart 10-16 implémente toujours la fonction new
pour retourner une
nouvelle instance de Paire<T>
(pour rappel dans la section
”Définir des méthodes” du chapitre 5 que Self
est
un alias de type pour le type du bloc impl
, qui est dans ce cas le
Paire<T>
). Mais dans le bloc impl
suivant, Paire<T>
implémente la
méthode afficher_comparaison
uniquement si son type interne T
implémente le
trait PartialOrd
qui active la comparaison et le trait Display
qui permet
l'affichage.
Fichier : src/lib.rs
use std::fmt::Display;
struct Paire<T> {
x: T,
y: T,
}
impl<T> Paire<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Paire<T> {
fn afficher_comparaison(&self) {
if self.x >= self.y {
println!("Le plus grand élément est x = {}", self.x);
} else {
println!("Le plus grand élément est y = {}", self.y);
}
}
}
Nous pouvons également implémenter un trait sur tout type qui implémente un
autre trait en particulier. L'implémentation d'un trait sur n'importe quel type
qui a un trait lié est appelée implémentation générale et est largement
utilisée dans la bibliothèque standard Rust. Par exemple, la bibliothèque
standard implémente le trait ToString
sur tous les types qui implémentent le
trait Display
. Le bloc impl
de la bibliothèque standard ressemble au code
suivant :
impl<T: Display> ToString for T {
// -- partie masquée ici --
}
Comme la bibliothèque standard a cette implémentation générale, nous pouvons
appeler la méthode to_string
définie par le trait ToString
sur n'importe
quel type qui implémente le trait Display
. Par exemple, nous pouvons
transformer les nombres entiers en leur équivalent dans une String
comme
ci-dessous car les entiers implémentent Display
:
#![allow(unused)] fn main() { let s = 3.to_string(); }
Les implémentations générales sont décrites dans la documentation du trait, dans la section “Implementors”.
Les traits et les traits liés nous permettent d'écrire du code qui utilise des paramètres de type génériques pour réduire la duplication de code, mais aussi pour indiquer au compilateur que nous voulons que le type générique ait un comportement particulier. Le compilateur peut ensuite utiliser les informations liées aux traits pour vérifier que tous les types concrets utilisés dans notre code suivent le comportement souhaité. Dans les langages typés dynamiquement, nous aurions une erreur à l'exécution si nous appelions une méthode sur un type qui n'implémenterait pas la méthode. Mais Rust décale l'apparition de ces erreurs au moment de la compilation afin de nous forcer à résoudre les problèmes avant même que notre code soit capable de s'exécuter. De plus, nous n'avons pas besoin d'écrire un code qui vérifie le comportement lors de l'exécution car nous l'avons déjà vérifié au moment de la compilation. Cela permet d'améliorer les performances sans avoir à sacrifier la flexibilité des types génériques.
Une autre sorte de générique que nous avons déjà utilisée est la durée de vie. Plutôt que de s'assurer qu'un type a le comportement que nous voulons, la durée de vie s'assure que les références sont en vigueur aussi longtemps que nous avons besoin qu'elles le soient. Nous allons voir à la page suivante comment la durée de vie fait cela.