Définir un comportement partagé avec les traits
Un trait définit la fonctionnalité qu’un type particulier possède et peut partager avec d’autres types. Nous pouvons utiliser les traits pour définir un comportement partagé de manière abstraite. Nous pouvons utiliser les trait bounds (limites de trait) pour spécifier qu’un type générique peut être n’importe quel type possédant un certain comportement.
Remarque : les traits sont similaires à une fonctionnalité souvent appelée interfaces dans d’autres langages, bien qu’avec quelques différences.
Définir un trait
Le comportement d’un type consiste en les méthodes que nous pouvons appeler sur ce type. Différents types partagent le même comportement si nous pouvons appeler les mêmes méthodes sur chacun de ces types. Les définitions de traits sont un moyen de regrouper des signatures de méthodes pour définir un ensemble de comportements nécessaires pour accomplir un certain objectif.
Par exemple, supposons que nous ayons plusieurs structs qui contiennent différents types et quantités de texte : une struct NewsArticle qui contient un article de presse classé dans un lieu particulier et un SocialPost qui peut avoir au maximum 280 caractères avec des métadonnées indiquant s’il s’agissait d’une nouvelle publication, d’un repartage ou d’une réponse à une autre publication.
Nous voulons créer une crate de bibliothèque d’agrégation de médias nommée aggregator qui peut afficher des résumés de données qui pourraient être stockées dans une instance de NewsArticle ou de SocialPost. Pour cela, nous avons besoin d’un résumé de chaque type, et nous demanderons ce résumé en appelant une méthode summarize sur une instance. L’encart 10-12 montre la définition d’un trait public Summary qui exprime ce comportement.
pub trait Summary {
fn summarize(&self) -> String;
}
Summary trait that consists of the behavior provided by a summarize methodIci, nous déclarons un trait en utilisant le mot-clé trait puis le nom du trait, qui est Summary dans ce cas. Nous déclarons aussi le trait comme pub pour que les crates qui dépendent de cette crate puissent aussi utiliser ce trait, comme nous le verrons dans quelques exemples. À l’intérieur des accolades, nous déclarons les signatures de méthodes qui décrivent les comportements des types qui implémentent ce trait, ce qui dans ce cas est fn summarize(&self) -> String.
Après la signature de la méthode, au lieu de fournir une implémentation entre accolades, nous utilisons un point-virgule. Chaque type implémentant ce trait doit fournir son propre comportement personnalisé pour le corps de la méthode. Le compilateur s’assurera que tout type qui possède le trait Summary aura la méthode summarize définie avec exactement cette signature.
Un trait peut avoir plusieurs méthodes dans son corps : les signatures de méthodes sont listées une par ligne, et chaque ligne se terminé par 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 Summary, nous pouvons l’implémenter sur les types de notre agrégateur de médias. L’encart 10-13 montre une implémentation du trait Summary sur la struct NewsArticle qui utilise le titre, l’auteur et le lieu pour créer la valeur de retour de summarize. Pour la struct SocialPost, nous définissons summarize comme le nom d’utilisateur suivi du texte entier de la publication, en supposant que le contenu de la publication est déjà limité à 280 caractères.
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Summary trait on the NewsArticle and SocialPost typesImplémenter un trait sur un type est similaire à implémenter des méthodes classiques. La différence est qu’après impl, nous mettons le nom du trait que nous voulons implémenter, puis nous utilisons le mot-clé for, puis nous spécifions le nom du type sur lequel nous voulons implémenter le trait. À l’intérieur du bloc impl, nous mettons les signatures de méthodes que la définition du trait a définies. Au lieu d’ajouter un point-virgule après chaque signature, nous utilisons des accolades et remplissons le corps de la méthode avec le comportement spécifique que nous voulons que les méthodes du trait aient pour le type particulier.
Maintenant que la bibliothèque a implémenté le trait Summary sur NewsArticle et SocialPost, les utilisateurs de la crate peuvent appeler les méthodes du trait sur des instances de NewsArticle et SocialPost de la même manière que nous appelons des méthodes classiques. La seule différence est que l’utilisateur doit importer le trait dans la portée ainsi que les types. Voici un exemple de la façon dont une crate binaire pourrait utiliser notre crate de bibliothèque aggregator : rust,ignore {{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-01-calling-trait-method/src/main.rs}}
use aggregator::{SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
Ce code affiche 1 new post: horse_ebooks: of course, as you probably already know, people.
D’autres crates qui dépendent de la crate aggregator peuvent aussi importer le trait Summary dans la portée pour implémenter Summary sur leurs propres types. Une restriction à noter est que nous ne pouvons implémenter un trait sur un type que si le trait ou le type, ou les deux, sont locaux à notre crate. Par exemple, nous pouvons implémenter des traits de la bibliothèque standard comme Display sur un type personnalisé comme SocialPost dans le cadre de la fonctionnalité de notre crate aggregator car le type SocialPost est local à notre crate aggregator. Nous pouvons aussi implémenter Summary sur Vec<T> dans notre crate aggregator car le trait Summary est local à notre crate aggregator.
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> dans notre crate aggregator, car Display et Vec<T> sont tous deux définis dans la bibliothèque standard et ne sont pas locaux à notre crate aggregator. Cette restriction fait partie d’une propriété appelée cohérence, et plus spécifiquement la règle de l’orphelin, ainsi nommée car le type parent n’est pas présent. Cette règle garantit que le code des autres ne peut pas casser votre code et vice versa. Sans cette règle, deux crates pourraient implémenter le même trait pour le même type, et Rust ne saurait pas quelle implémentation utiliser.
Utiliser les implémentations par défaut
Parfois, il est utile d’avoir un comportement par défaut pour certaines ou toutes les méthodes d’un trait au lieu d’exiger des implémentations pour toutes les méthodes sur chaque type. Ensuite, lorsque nous implémentons le trait sur un type particulier, nous pouvons conserver ou remplacer le comportement par défaut de chaque méthode.
Dans l’encart 10-14, nous spécifions une chaîne de caractères par défaut pour la méthode summarize du trait Summary au lieu de définir uniquement la signature de la méthode, comme nous l’avons fait dans l’encart 10-12.
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Summary trait with a default implementation of the summarize methodPour utiliser une implémentation par défaut afin de résumer les instances de NewsArticle, nous spécifions un bloc impl vide avec impl Summary for NewsArticle {}.
Même si nous ne définissons plus directement la méthode summarize sur NewsArticle, nous avons fourni une implémentation par défaut et spécifié que NewsArticle implémente le trait Summary. En conséquence, nous pouvons toujours appeler la méthode summarize sur une instance de NewsArticle, comme ceci : rust,ignore {{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-02-calling-default-impl/src/main.rs:here}}
use aggregator::{self, NewsArticle, Summary};
fn main() {
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \n hockey team in the NHL.",
),
};
println!("New article available! {}", article.summarize());
}
Ce code affiche New article available! (Read more...).
Créer une implémentation par défaut ne nous oblige pas à modifier quoi que ce soit dans l’implémentation de Summary sur SocialPost dans l’encart 10-13. La raison est que la syntaxe pour remplacer une implémentation par défaut est la même que celle pour implémenter une méthode de trait 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. De cette façon, un trait peut fournir beaucoup de fonctionnalités utiles et n’exiger des implémenteurs qu’ils ne spécifient qu’une petite partie. Par exemple, nous pourrions définir le trait Summary avec une méthode summarize_author dont l’implémentation est requise, puis définir une méthode summarize qui à une implémentation par défaut qui appelle la méthode summarize_author : rust,noplayground {{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-03-default-impl-calls-other-methods/src/lib.rs:here}}
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
Pour utiliser cette version de Summary, nous n’avons besoin de définir que summarize_author lorsque nous implémentons le trait sur un type : rust,ignore {{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-03-default-impl-calls-other-methods/src/lib.rs:impl}}
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
Après avoir défini summarize_author, nous pouvons appeler summarize sur des instances de la struct SocialPost, et l’implémentation par défaut de summarize appellera la définition de summarize_author que nous avons fournie. Comme nous avons implémenté summarize_author, le trait Summary nous a donné le comportement de la méthode summarize sans que nous ayons besoin d’écrire du code supplémentaire. Voici à quoi cela ressemble : rust,ignore {{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-03-default-impl-calls-other-methods/src/main.rs:here}}
use aggregator::{self, SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
Ce code affiche 1 new post: (Read more from @horse_ebooks...).
Notez qu’il n’est pas possible d’appeler l’implémentation par défaut depuis une implémentation qui remplace cette même méthode.
Utiliser les traits comme paramètres
Maintenant que vous savez comment définir et implémenter des traits, nous pouvons explorer comment utiliser les traits pour définir des fonctions qui acceptent de nombreux types différents. Nous utiliserons le trait Summary que nous avons implémenté sur les types NewsArticle et SocialPost dans l’encart 10-13 pour définir une fonction notify qui appelle la méthode summarize sur son paramètre item, qui est d’un certain type implémentant le trait Summary. Pour ce faire, nous utilisons la syntaxe impl Trait, comme ceci : rust,ignore {{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-04-traits-as-parameters/src/lib.rs:here}}
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
Au lieu d’un type concret pour le paramètre item, nous spécifions 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 notify, nous pouvons appeler n’importe quelle méthode sur item provenant du trait Summary, comme summarize. Nous pouvons appeler notify et passer n’importe quelle instance de NewsArticle ou SocialPost. Le code qui appelle la fonction avec n’importe quel autre type, comme un String ou un i32, ne compilera pas, car ces types n’implémentent pas Summary.
La syntaxe des trait bounds
La syntaxe impl Trait fonctionne pour les cas simples mais est en fait du sucre syntaxique pour une forme plus longue connue sous le nom de trait bound ; elle ressemble à ceci :
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
Cette forme plus longue est équivalente à l’exemple de la section précédente mais est plus verbeuse. Nous plaçons les trait bounds avec la déclaration du paramètre de type générique après un deux-points et entre chevrons.
La syntaxe impl Trait est pratique et permet un code plus concis dans les cas simples, tandis que la syntaxe plus complète des trait bounds peut exprimer plus de complexité dans d’autres cas. Par exemple, nous pouvons avoir deux paramètres qui implémentent Summary. Le faire avec la syntaxe impl Trait ressemble à ceci :
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
Utiliser impl Trait est approprié si nous voulons que cette fonction permette à item1 et item2 d’avoir des types différents (tant que les deux types implémentent Summary). Si nous voulons forcer les deux paramètres à avoir le même type, cependant, nous devons utiliser un trait bound, comme ceci :
pub fn notify<T: Summary>(item1: &T, item2: &T) {
Le type générique T spécifié comme type des paramètres item1 et item2 contraint la fonction de telle sorte que le type concret de la valeur passée en argument pour item1 et item2 doit être le même.
Trait bounds multiples avec la syntaxe +
Nous pouvons aussi spécifier plus d’un trait bound. Supposons que nous voulions que notify utilise le formatage d’affichage ainsi que summarize sur item : nous spécifions dans la définition de notify que item doit implémenter à la fois Display et Summary. Nous pouvons le faire en utilisant la syntaxe + :
pub fn notify(item: &(impl Summary + Display)) {
La syntaxe + est aussi valide avec les trait bounds sur les types génériques :
pub fn notify<T: Summary + Display>(item: &T) {
Avec les deux trait bounds spécifiés, le corps de notify peut appeler summarize et utiliser {} pour formater item.
Des trait bounds plus clairs avec les clauses where
Utiliser trop de trait bounds à ses inconvénients. Chaque générique à ses propres trait bounds, donc les fonctions avec plusieurs paramètres de type générique peuvent contenir beaucoup d’informations de trait bounds entre le nom de la fonction et sa liste de paramètres, rendant la signature de la fonction difficile à lire. Pour cette raison, Rust dispose d’une syntaxe alternative pour spécifier les trait bounds dans une clause where après la signature de la fonction. Ainsi, au lieu d’écrire ceci :
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
nous pouvons utiliser une clause where, comme ceci : rust,ignore {{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-07-where-clause/src/lib.rs:here}}
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
unimplemented!()
}
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 proches les uns des autres, similaire à une fonction sans beaucoup de trait bounds.
Retourner des types qui implémentent des traits
Nous pouvons aussi utiliser la syntaxe impl Trait en position de retour pour retourner une valeur d’un certain type qui implémente un trait, comme montré ici : rust,ignore {{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-05-returning-impl-trait/src/lib.rs:here}}
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn returns_summarizable() -> impl Summary {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
En utilisant impl Summary comme type de retour, nous spécifions que la fonction returns_summarizable retourné un certain type qui implémente le trait Summary sans nommer le type concret. Dans ce cas, returns_summarizable retourné un SocialPost, mais le code qui appelle cette fonction n’a pas besoin de le savoir.
La possibilité de spécifier un type de retour uniquement par le trait qu’il implémente est particulièrement utile dans le contexte des fermetures (closures) et des itérateurs, que nous couvrons au chapitre 13. Les fermetures et les itérateurs créent des types que seul le compilateur connaît ou des types très longs à spécifier. La syntaxe impl Trait vous permet de spécifier de manière concise qu’une fonction retourné un certain type qui implémente le trait Iterator sans avoir besoin d’écrire un type très long.
Cependant, vous ne pouvez utiliser impl Trait que si vous retournez un seul type. Par exemple, ce code qui retourné soit un NewsArticle soit un SocialPost avec le type de retour spécifié comme impl Summary ne fonctionnerait pas : rust,ignore,does_not_compile {{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-06-impl-trait-returns-one-type/src/lib.rs:here}}
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from(
"Penguins win the Stanley Cup Championship!",
),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \n hockey team in the NHL.",
),
}
} else {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
}
section of Chapter 18. –> Retourner soit un NewsArticle soit un SocialPost n’est pas autorisé en raison de restrictions liées à 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 la section [« Utiliser les objets trait pour abstraire un comportement partagé »][trait-objects] du chapitre 18.
Utiliser les trait bounds pour implémenter des méthodes conditionnellement
En utilisant un trait bound avec un bloc impl qui utilise des paramètres de type générique, nous pouvons implémenter des méthodes conditionnellement pour les types qui implémentent les traits spécifiés. Par exemple, le type Pair<T> dans l’encart 10-15 implémente toujours la fonction new pour retourner une nouvelle instance de Pair<T> (rappelez-vous de la section [« La syntaxe des méthodes »][methods] du chapitre 5 que Self est un alias de type pour le type du bloc impl, qui dans ce cas est Pair<T>). Mais dans le bloc impl suivant, Pair<T> n’implémente la méthode cmp_display que si son type interne T implémente le trait PartialOrd qui permet la comparaison et le trait Display qui permet l’affichage.
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
Nous pouvons aussi implémenter conditionnellement un trait pour n’importe quel type qui implémente un autre trait. Les implémentations d’un trait sur n’importe quel type qui satisfait les trait bounds sont appelées implémentations couvertures (blanket implémentations) et sont largement utilisées dans la bibliothèque standard de Rust. Par exemple, la bibliothèque standard implémente le trait ToString sur n’importe quel type qui implémente le trait Display. Le bloc impl dans la bibliothèque standard ressemble à ce code :
impl<T: Display> ToString for T {
// --snip--
}
Comme la bibliothèque standard possède cette implémentation couverture, 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 convertir des entiers en leurs valeurs String correspondantes comme ceci car les entiers implémentent Display :
#![allow(unused)]
fn main() {
let s = 3.to_string();
}
Les implémentations couvertures apparaissent dans la documentation du trait dans la section « Implementors ».
Les traits et les trait bounds nous permettent d’écrire du code qui utilise des paramètres de type générique pour réduire la duplication tout en spécifiant au compilateur que nous voulons que le type générique ait un comportement particulier. Le compilateur peut alors utiliser les informations des trait bounds pour vérifier que tous les types concrets utilisés avec notre code fournissent le comportement correct. Dans les langages à typage dynamique, nous obtiendrions une erreur à l’exécution si nous appelions une méthode sur un type qui ne définit pas cette méthode. Mais Rust déplace ces erreurs au moment de la compilation pour que nous soyons obligés de corriger les problèmes avant même que notre code ne puisse s’exécuter. De plus, nous n’avons pas besoin d’écrire du code qui vérifie le comportement à l’exécution, car nous l’avons déjà vérifié à la compilation. Cela améliore les performances sans avoir à renoncer à la flexibilité des génériques.