Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Les traits avancés

Nous avons d’abord couvert les traits dans la section « Définir un comportement partagé avec les traits » du chapitre 10, mais nous n’avons pas discuté des détails plus avancés. Maintenant que vous en savez plus sur Rust, nous pouvons entrer dans le vif du sujet.

Définir des traits avec des types associés

Les types associés connectent un type générique de substitution avec un trait de sorte que les définitions des méthodes du trait puissent utiliser ces types de substitution dans leurs signatures. L’implémenteur d’un trait spécifiera le type concret à utiliser à la place du type de substitution pour l’implémentation particulière. De cette manière, nous pouvons définir un trait qui utilise certains types sans avoir besoin de savoir exactement quels sont ces types jusqu’à ce que le trait soit implémenté.

Nous avons décrit la plupart des fonctionnalités avancées de ce chapitre comme étant rarement nécessaires. Les types associés se situent quelque part au milieu : ils sont utilisés plus rarement que les fonctionnalités expliquées dans le reste du livre mais plus couramment que beaucoup d’autres fonctionnalités discutées dans ce chapitre.

Un exemple de trait avec un type associé est le trait Iterator fourni par la bibliothèque standard. Le type associé est nommé Item et représente le type des valeurs sur lesquelles le type implémentant le trait Iterator itère. La définition du trait Iterator est celle montrée dans l’encart 20-13.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
Listing 20-13: The definition of the Iterator trait that has an associated type Item

Le type Item est un type de substitution, et la définition de la méthode next montre qu’elle retournera des valeurs de type Option<Self::Item>. Les implémenteurs du trait Iterator spécifieront le type concret pour Item, et la méthode next retournera une Option contenant une valeur de ce type concret.

Les types associés pourraient sembler être un concept similaire aux génériques, dans la mesure où ces derniers nous permettent de définir une fonction sans spécifier quels types elle peut traiter. Pour examiner la différence entre les deux concepts, nous allons regarder une implémentation du trait Iterator sur un type nommé Counter qui spécifie que le type Item est u32 :

Filename: src/lib.rs
struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

Cette syntaxe semble comparable à celle des génériques. Alors, pourquoi ne pas simplement définir le trait Iterator avec des génériques, comme montré dans l’encart 20-14 ?

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}
Listing 20-14: A hypothetical definition of the Iterator trait using generics

La différence est que lorsque nous utilisons des génériques, comme dans l’encart 20-14, nous devons annoter les types dans chaque implémentation ; parce que nous pourrions aussi implémenter Iterator<String> for Counter ou n’importe quel autre type, nous pourrions avoir plusieurs implémentations d’Iterator pour Counter. En d’autres termes, lorsqu’un trait à un paramètre générique, il peut être implémenté pour un type plusieurs fois, en changeant les types concrets des paramètres de type générique à chaque fois. Lorsque nous utilisons la méthode next sur Counter, nous devrions fournir des annotations de type pour indiquer quelle implémentation d’Iterator nous voulons utiliser.

Avec les types associés, nous n’avons pas besoin d’annoter les types, car nous ne pouvons pas implémenter un trait sur un type plusieurs fois. Dans l’encart 20-13 avec la définition qui utilise des types associés, nous ne pouvons choisir le type d’Item qu’une seule fois car il ne peut y avoir qu’un seul impl Iterator for Counter. Nous n’avons pas à spécifier que nous voulons un itérateur de valeurs u32 partout où nous appelons next sur Counter.

Les types associés font aussi partie du contrat du trait : les implémenteurs du trait doivent fournir un type pour remplacer le type de substitution associé. Les types associés ont souvent un nom qui décrit comment le type sera utilisé, et documenter le type associé dans la documentation de l’API est une bonne pratique.

Utiliser les paramètres de type générique par défaut et la surcharge d’opérateurs

Lorsque nous utilisons des paramètres de type générique, nous pouvons spécifier un type concret par défaut pour le type générique. Cela élimine la nécessité pour les implémenteurs du trait de spécifier un type concret si le type par défaut convient. Vous spécifiez un type par défaut en déclarant un type générique avec la syntaxe <TypeDeSubstitution=TypeConcret>.

Un excellent exemple de situation où cette technique est utile est la surcharge d’opérateurs, dans laquelle vous personnalisez le comportement d’un opérateur (comme +) dans des situations particulières.

Rust ne vous permet pas de créer vos propres opérateurs ni de surcharger des opérateurs arbitraires. Mais vous pouvez surcharger les opérations et les traits correspondants listés dans std::ops en implémentant les traits associés à l’opérateur. Par exemple, dans l’encart 20-15, nous surchargeons l’opérateur + pour additionner deux instances de Point. Nous faisons cela en implémentant le trait Add sur une structure Point.

Filename: src/main.rs
use std::ops::Add;

#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}
Listing 20-15: Implementing the Add trait to overload the + operator for Point instances

La méthode add additionne les valeurs x de deux instances de Point et les valeurs y de deux instances de Point pour créer un nouveau Point. Le trait Add possède un type associé nommé Output qui détermine le type retourné par la méthode add.

Le type générique par défaut dans ce code se trouve dans le trait Add. Voici sa définition :

#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}
}

Ce code devrait vous sembler généralement familier : un trait avec une méthode et un type associé. La partie nouvelle est Rhs=Self : cette syntaxe s’appelle les paramètres de type par défaut. Le paramètre de type générique Rhs (abréviation de “right-hand side”, côté droit) définit le type du paramètre rhs dans la méthode add. Si nous ne spécifions pas de type concret pour Rhs lorsque nous implémentons le trait Add, le type de Rhs sera par défaut Self, qui sera le type sur lequel nous implémentons Add.

Lorsque nous avons implémenté Add pour Point, nous avons utilisé la valeur par défaut pour Rhs car nous voulions additionner deux instances de Point. Regardons un exemple d’implémentation du trait Add où nous voulons personnaliser le type Rhs plutôt que d’utiliser la valeur par défaut.

Nous avons deux structures, Millimeters et Meters, contenant des valeurs dans des unités différentes. Cet enveloppement fin d’un type existant dans une autre structure est connu sous le nom de patron newtype, que nous décrivons plus en détail dans la section [“Implémenter des traits externes avec le patron newtype”][newtype]. Nous voulons additionner des valeurs en millimètres à des valeurs en mètres et que l’implémentation d’Add fasse la conversion correctement. Nous pouvons implémenter Add pour Millimeters avec Meters comme Rhs, comme montré dans l’encart 20-16.

Filename: src/lib.rs
use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}
Listing 20-16: Implementing the Add trait on Millimeters to add Millimeters and Meters

Pour additionner Millimeters et Meters, nous spécifions impl Add<Meters> pour définir la valeur du paramètre de type Rhs au lieu d’utiliser la valeur par défaut Self.

Vous utiliserez les paramètres de type par défaut de deux manières principales :

  1. Pour étendre un type sans casser le code existant
  2. Pour permettre la personnalisation dans des cas spécifiques dont la plupart des utilisateurs n’auront pas besoin

Le trait Add de la bibliothèque standard est un exemple du second objectif : généralement, vous additionnerez deux types identiques, mais le trait Add offre la possibilité de personnaliser au-delà. Utiliser un paramètre de type par défaut dans la définition du trait Add signifie que vous n’avez pas à spécifier le paramètre supplémentaire la plupart du temps. En d’autres termes, un peu de code standard d’implémentation n’est pas nécessaire, rendant le trait plus facile à utiliser.

Le premier objectif est similaire au second mais en sens inverse : si vous voulez ajouter un paramètre de type à un trait existant, vous pouvez lui donner une valeur par défaut pour permettre l’extension de la fonctionnalité du trait sans casser le code d’implémentation existant.

Lever l’ambiguïté entre des méthodes portant le même nom

Rien en Rust n’empêche un trait d’avoir une méthode portant le même nom que la méthode d’un autre trait, et Rust ne vous empêche pas non plus d’implémenter les deux traits sur un même type. Il est aussi possible d’implémenter une méthode directement sur le type avec le même nom que les méthodes des traits.

Lors de l’appel de méthodes portant le même nom, vous devrez indiquer à Rust laquelle vous souhaitez utiliser. Considérez le code de l’encart 20-17 où nous avons défini deux traits, Pilot et Wizard, qui ont tous deux une méthode appelée fly. Nous implémentons ensuite les deux traits sur un type Human qui possède déjà une méthode nommée fly. Chaque méthode fly fait quelque chose de différent.

Filename: src/main.rs
trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {}
Listing 20-17: Two traits are defined to have a fly method and are implemented on the Human type, and a fly method is implemented on Human directly.

Lorsque nous appelons fly sur une instance de Human, le compilateur appelle par défaut la méthode directement implémentée sur le type, comme montré dans l’encart 20-18.

Filename: src/main.rs
trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    person.fly();
}
Listing 20-18: Calling fly on an instance of Human

L’exécution de ce code affichera *waving arms furiously*, montrant que Rust a appelé la méthode fly implémentée directement sur Human.

Pour appeler les méthodes fly du trait Pilot ou du trait Wizard, nous devons utiliser une syntaxe plus explicite pour spécifier quelle méthode fly nous voulons dire. L’encart 20-19 illustre cette syntaxe.

Filename: src/main.rs
trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}
Listing 20-19: Specifying which trait’s fly method we want to call

Spécifier le nom du trait avant le nom de la méthode clarifie pour Rust quelle implémentation de fly nous voulons appeler. Nous pourrions aussi écrire Human::fly(&person), qui est équivalent au person.fly() que nous avons utilisé dans l’encart 20-19, mais c’est un peu plus long à écrire si nous n’avons pas besoin de lever l’ambiguïté.

L’exécution de ce code affiche le résultat suivant : console {{#include ../listings/ch13-functional-features/listing-13-01/output.txt}}

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*

Parce que la méthode fly prend un paramètre self, si nous avions deux types qui implémentent tous les deux un même trait, Rust pourrait déterminer quelle implémentation du trait utiliser en se basant sur le type de self.

Cependant, les fonctions associées qui ne sont pas des méthodes n’ont pas de paramètre self. Lorsqu’il y à plusieurs types ou traits qui définissent des fonctions non-méthodes avec le même nom de fonction, Rust ne sait pas toujours quel type vous voulez dire à moins que vous n’utilisiez la syntaxe pleinement qualifiée. Par exemple, dans l’encart 20-20, nous créons un trait pour un refuge animalier qui veut nommer tous les chiots Spot. Nous créons un trait Animal avec une fonction associée non-méthode baby_name. Le trait Animal est implémenté pour la structure Dog, sur laquelle nous fournissons aussi directement une fonction associée non-méthode baby_name.

Filename: src/main.rs
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
}
Listing 20-20: A trait with an associated function and a type with an associated function of the same name that also implements the trait

Nous implémentons le code pour nommer tous les chiots Spot dans la fonction associée baby_name définie sur Dog. Le type Dog implémente aussi le trait Animal, qui décrit les caractéristiques communes à tous les animaux. Les bébés chiens s’appellent des chiots, et cela est exprimé dans l’implémentation du trait Animal sur Dog dans la fonction baby_name associée au trait Animal.

Dans main, nous appelons la fonction Dog::baby_name, qui appelle la fonction associée définie directement sur Dog. Ce code affiche ce qui suit : console {{#include ../listings/ch20-advanced-features/listing-20-20/output.txt}}

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
     Running `target/debug/traits-example`
A baby dog is called a Spot

Cette sortie n’est pas ce que nous voulions. Nous voulons appeler la fonction baby_name qui fait partie du trait Animal que nous avons implémenté sur Dog pour que le code affiche A baby dog is called a puppy. La technique de spécification du nom du trait que nous avons utilisée dans l’encart 20-19 ne nous aide pas ici ; si nous changeons main avec le code de l’encart 20-21, nous obtiendrons une erreur de compilation.

Filename: src/main.rs
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Animal::baby_name());
}
Listing 20-21: Attempting to call the baby_name function from the Animal trait, but Rust doesn’t know which implementation to use

Parce que Animal::baby_name n’a pas de paramètre self, et qu’il pourrait y avoir d’autres types qui implémentent le trait Animal, Rust ne peut pas déterminer quelle implémentation d’Animal::baby_name nous voulons. Nous obtiendrons cette erreur du compilateur : console {{#include ../listings/ch20-advanced-features/listing-20-21/output.txt}}

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
  --> src/main.rs:20:43
   |
 2 |     fn baby_name() -> String;
   |     ------------------------- `Animal::baby_name` defined here
...
20 |     println!("A baby dog is called a {}", Animal::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
   |
help: use the fully-qualified path to the only available implementation
   |
20 |     println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
   |                                           +++++++       +

For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error

Pour lever l’ambiguïté et dire à Rust que nous voulons utiliser l’implémentation d’Animal pour Dog par opposition à l’implémentation d’Animal pour un autre type, nous devons utiliser la syntaxe pleinement qualifiée. L’encart 20-22 montre comment utiliser la syntaxe pleinement qualifiée.

Filename: src/main.rs
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
Listing 20-22: Using fully qualified syntax to specify that we want to call the baby_name function from the Animal trait as implemented on Dog

Nous fournissons à Rust une annotation de type entre les chevrons, qui indique que nous voulons appeler la méthode baby_name du trait Animal telle qu’implémentée sur Dog en disant que nous voulons traiter le type Dog comme un Animal pour cet appel de fonction. Ce code affichera maintenant ce que nous voulons : console {{#include ../listings/ch20-advanced-features/listing-20-22/output.txt}}

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/traits-example`
A baby dog is called a puppy

En général, la syntaxe pleinement qualifiée est définie comme suit :

<Type as Trait>::function(receiver_if_method, next_arg, ...);

Pour les fonctions associées qui ne sont pas des méthodes, il n’y aurait pas de receiver : il n’y aurait que la liste des autres arguments. Vous pourriez utiliser la syntaxe pleinement qualifiée partout où vous appelez des fonctions ou des méthodes. Cependant, vous êtes autorisé à omettre toute partie de cette syntaxe que Rust peut déduire d’autres informations dans le programme. Vous n’avez besoin d’utiliser cette syntaxe plus verbeuse que dans les cas où il y à plusieurs implémentations utilisant le même nom et où Rust a besoin d’aide pour identifier quelle implémentation vous voulez appeler.

Utiliser les supertraits

Parfois, vous pourriez écrire une définition de trait qui dépend d’un autre trait : pour qu’un type implémente le premier trait, vous voulez exiger que ce type implémente aussi le second trait. Vous feriez cela pour que la définition de votre trait puisse utiliser les éléments associés du second trait. Le trait sur lequel votre définition de trait s’appuie est appelé un supertrait de votre trait.

Par exemple, disons que nous voulons créer un trait OutlinePrint avec une méthode outline_print qui affichera une valeur donnée formatée de sorte qu’elle soit encadrée d’astérisques. C’est-à-dire, étant donné une structure Point qui implémente le trait Display de la bibliothèque standard pour produire (x, y), lorsque nous appelons outline_print sur une instance de Point qui a 1 pour x et 3 pour y, elle devrait afficher ce qui suit :

**********
*        *
* (1, 3) *
*        *
**********

Dans l’implémentation de la méthode outline_print, nous voulons utiliser la fonctionnalité du trait Display. Par conséquent, nous devons spécifier que le trait OutlinePrint ne fonctionnera que pour les types qui implémentent aussi Display et fournissent la fonctionnalité dont OutlinePrint a besoin. Nous pouvons faire cela dans la définition du trait en spécifiant OutlinePrint: Display. Cette technique est similaire à l’ajout d’une contrainte de trait au trait. L’encart 20-23 montre une implémentation du trait OutlinePrint.

Filename: src/main.rs
use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

fn main() {}
Listing 20-23: Implementing the OutlinePrint trait that requires the functionality from Display

Parce que nous avons spécifié qu’OutlinePrint nécessite le trait Display, nous pouvons utiliser la fonction to_string qui est automatiquement implémentée pour tout type qui implémente Display. Si nous essayions d’utiliser to_string sans ajouter un deux-points et spécifier le trait Display après le nom du trait, nous obtiendrions une erreur disant qu’aucune méthode nommée to_string n’a été trouvée pour le type &Self dans la portée actuelle.

Voyons ce qui se passe lorsque nous essayons d’implémenter OutlinePrint sur un type qui n’implémente pas Display, comme la structure Point :

Filename: src/main.rs
use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

Nous obtenons une erreur disant que Display est requis mais n’est pas implémenté : console {{#include ../listings/ch20-advanced-features/no-listing-02-impl-outlineprint-for-point/output.txt}}

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:20:23
   |
20 | impl OutlinePrint for Point {}
   |                       ^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
   |
note: required by a bound in `OutlinePrint`
  --> src/main.rs:3:21
   |
 3 | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint`

error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:24:7
   |
24 |     p.outline_print();
   |       ^^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
   |
note: required by a bound in `OutlinePrint::outline_print`
  --> src/main.rs:3:21
   |
 3 | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
 4 |     fn outline_print(&self) {
   |        ------------- required by a bound in this associated function

For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors

Pour corriger cela, nous implémentons Display sur Point et satisfaisons la contrainte qu’OutlinePrint exige, comme suit :

Filename: src/main.rs
trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

Ensuite, l’implémentation du trait OutlinePrint sur Point compilera avec succès, et nous pourrons appeler outline_print sur une instance de Point pour l’afficher dans un cadre d’astérisques.

Implémenter des traits externes avec le patron newtype

Dans la section [“Implémenter un trait sur un type”][implementing-a-trait-on-a-type] du chapitre 10, nous avons mentionné la règle de l’orphelin qui stipule que nous ne sommes autorisés à implémenter un trait sur un type que si soit le trait, soit le type, soit les deux, sont locaux à notre crate. Il est possible de contourner cette restriction en utilisant le patron newtype, qui consiste à créer un nouveau type dans une structure tuple. (Nous avons couvert les structures tuple dans la section [“Créer différents types avec les structures tuple”][tuple-structs] du chapitre 5.) La structure tuple aura un seul champ et sera un enveloppement fin autour du type pour lequel nous voulons implémenter un trait. Ensuite, le type enveloppeur est local à notre crate, et nous pouvons implémenter le trait sur l’enveloppeur. Newtype est un terme qui provient du langage de programmation Haskell. Il n’y a pas de pénalité de performance à l’exécution pour l’utilisation de ce patron, et le type enveloppeur est éliminé au moment de la compilation.

À titre d’exemple, disons que nous voulons implémenter Display sur Vec<T>, ce que la règle de l’orphelin nous empêche de faire directement car le trait Display et le type Vec<T> sont définis en dehors de notre crate. Nous pouvons créer une structure Wrapper qui contient une instance de Vec<T> ; ensuite, nous pouvons implémenter Display sur Wrapper et utiliser la valeur Vec<T>, comme montré dans l’encart 20-24.

Filename: src/main.rs
use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {w}");
}
Listing 20-24: Creating a Wrapper type around Vec<String> to implement Display

L’implémentation de Display utilise self.0 pour accéder au Vec<T> interne car Wrapper est une structure tuple et Vec<T> est l’élément à l’indice 0 dans le tuple. Ensuite, nous pouvons utiliser la fonctionnalité du trait Display sur Wrapper.

section in Chapter 15). If we didn’t want the Wrapper type to have all the methods of the inner type—for example, to restrict the Wrapper type’s behavior—we would have to implement just the methods we do want manually. –> L’inconvénient d’utiliser cette technique est que Wrapper est un nouveau type, il n’a donc pas les méthodes de la valeur qu’il contient. Nous devrions implémenter toutes les méthodes de Vec<T> directement sur Wrapper de sorte que les méthodes délèguent à self.0, ce qui nous permettrait de traiter Wrapper exactement comme un Vec<T>. Si nous voulions que le nouveau type ait toutes les méthodes du type interne, implémenter le trait Deref sur Wrapper pour retourner le type interne serait une solution (nous avons discuté de l’implémentation du trait Deref dans la section [“Traiter les pointeurs intelligents comme des références normales”][smart-pointer-deref] du chapitre 15). Si nous ne voulions pas que le type Wrapper ait toutes les méthodes du type interne – par exemple, pour restreindre le comportement du type Wrapper – nous devrions implémenter manuellement seulement les méthodes que nous souhaitons.

Ce patron newtype est aussi utile même quand les traits ne sont pas impliqués. Changeons de sujet et examinons quelques manières avancées d’interagir avec le système de types de Rust.