Exploration avancée des traits Rust

RustRustBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Bienvenue dans Advanced Traits. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous approfondirons les détails plus avancés des traits qui ont été précédemment abordés dans "Traits: Définition d'un comportement partagé", maintenant que vous avez une meilleure compréhension du Rust.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/DataTypesGroup -.-> rust/type_casting("Type Conversion and Casting") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100448{{"Exploration avancée des traits Rust"}} rust/integer_types -.-> lab-100448{{"Exploration avancée des traits Rust"}} rust/string_type -.-> lab-100448{{"Exploration avancée des traits Rust"}} rust/type_casting -.-> lab-100448{{"Exploration avancée des traits Rust"}} rust/function_syntax -.-> lab-100448{{"Exploration avancée des traits Rust"}} rust/expressions_statements -.-> lab-100448{{"Exploration avancée des traits Rust"}} rust/method_syntax -.-> lab-100448{{"Exploration avancée des traits Rust"}} rust/traits -.-> lab-100448{{"Exploration avancée des traits Rust"}} rust/operator_overloading -.-> lab-100448{{"Exploration avancée des traits Rust"}} end

Advanced Traits

Nous avons abordé les traits pour la première fois dans "Traits: Définition d'un comportement partagé", mais nous n'avons pas discuté des détails plus avancés. Maintenant que vous connaissez mieux le Rust, nous pouvons entrer dans le détail.

Types associées

Les types associés relient un type générique à un trait de sorte que les définitions de méthodes de trait puissent utiliser ces types génériques dans leurs signatures. L'implémentateur d'un trait spécifiera le type concret à utiliser au lieu du type générique pour une 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 avant que le trait ne 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 au milieu : ils sont utilisés moins fréquemment que les fonctionnalités expliquées dans le reste du livre, mais plus fréquemment que de nombreuses 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 itère le type implémentant le trait Iterator. La définition du trait Iterator est comme montrée dans la Liste 19-12.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

Liste 19-12 : La définition du trait Iterator qui a un type associé Item

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

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

Nom de fichier : src/lib.rs

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        --snip--

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 la Liste 19-13?

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

Liste 19-13 : Une définition hypothétique du trait Iterator utilisant des génériques

La différence est que lorsqu'on utilise des génériques, comme dans la Liste 19-13, nous devons annoter les types dans chaque implémentation ; car nous pouvons également implémenter Iterator<``String``> pour Counter ou tout autre type, nous pourrions avoir plusieurs implémentations de Iterator pour Counter. En d'autres termes, lorsqu'un trait a un paramètre générique, il peut être implémenté pour un type plusieurs fois, en changeant les types concret des paramètres de type générique à chaque fois. Lorsque nous utilisons la méthode next sur Counter, nous devons fournir des annotations de type pour indiquer quelle implémentation de 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 pour un type plusieurs fois. Dans la Liste 19-12 avec la définition qui utilise des types associés, nous ne pouvons choisir le type de Item qu'une seule fois car il ne peut y avoir qu'une seule impl Iterator for Counter. Nous n'avons pas besoin de spécifier que nous voulons un itérateur de valeurs de type u32 partout où nous appelons next sur Counter.

Les types associés deviennent également partie du contrat du trait : les implémentateurs du trait doivent fournir un type pour remplacer le type générique associé. Les types associés ont souvent un nom qui décrit la manière dont le type sera utilisé, et il est une bonne pratique de documenter le type associé dans la documentation de l'API.

Paramètres de type génériques par défaut et surcharge d'opérateurs

Lorsque nous utilisons des paramètres de type génériques, nous pouvons spécifier un type concret par défaut pour le type générique. Cela élimine la nécessité pour les implémentateurs du trait de spécifier un type concret si le type par défaut convient. Vous spécifiez un type par défaut lors de la déclaration d'un type générique avec la syntaxe <TypeGénérique=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 (tel que +) dans des situations particulières.

Rust ne vous permet pas de créer vos propres opérateurs ou 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 la Liste 19-14, nous surchargeons l'opérateur + pour additionner deux instances de Point. Nous le faisons en implémentant le trait Add sur une structure Point.

Nom de fichier : 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 }
    );
}

Liste 19-14 : Implémentation du trait Add pour surcharger l'opérateur + pour les instances de Point

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

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

trait Add<Rhs=Self> {
    type Output;

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

Ce code devrait vous paraître globalement familier : un trait avec une méthode et un type associé. La partie nouvelle est Rhs=Self : cette syntaxe est appelée paramètres de type par défaut. Le paramètre de type générique Rhs (abrégé de "right-hand side") définit le type du paramètre rhs dans la méthode add. Si nous ne spécifions pas un type concret pour Rhs lorsque nous implémentons le trait Add, le type de Rhs sera la valeur 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. Considérons 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, qui stockent des valeurs dans des unités différentes. Ce conditionnement mince d'un type existant dans une autre structure est connu sous le nom de nouveau modèle de type, que nous décrivons en détail dans "Utilisation du nouveau modèle de type pour implémenter des traits externes sur des types externes". Nous voulons ajouter des valeurs en millimètres à des valeurs en mètres et que l'implémentation de Add effectue correctement la conversion. Nous pouvons implémenter Add pour Millimeters avec Meters comme Rhs, comme montré dans la Liste 19-15.

Nom de fichier : 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))
    }
}

Liste 19-15 : Implémentation du trait Add sur Millimeters pour ajouter Millimeters et Meters

Pour ajouter 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 une personnalisation dans des cas spécifiques que la plupart des utilisateurs n'auront pas besoin

Le trait Add de la bibliothèque standard est un exemple du second but : généralement, vous additionnerez deux types similaires, mais le trait Add offre la possibilité de personnaliser au-delà de cela. L'utilisation d'un paramètre de type par défaut dans la définition du trait Add signifie que vous n'avez pas besoin de spécifier le paramètre supplémentaire la plupart du temps. En d'autres termes, un peu de boilerplate d'implémentation n'est pas nécessaire, ce qui facilite l'utilisation du trait.

Le premier but est similaire au second mais à l'envers : 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.

Distinction entre des méthodes de même nom

Rien en Rust n'empêche un trait d'avoir une méthode de même nom qu'une méthode d'un autre trait, ni Rust ne vous empêche d'implémenter les deux traits sur un même type. Il est également possible d'implémenter directement une méthode sur le type avec le même nom que des méthodes provenant de traits.

Lorsque vous appelez des méthodes de même nom, vous devrez dire à Rust laquelle vous voulez utiliser. Considérez le code de la Liste 19-16 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 a déjà une méthode nommée fly implémentée sur lui. Chaque méthode fly fait quelque chose de différent.

Nom de fichier : 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*");
    }
}

Liste 19-16 : Deux traits sont définis pour avoir une méthode fly et sont implémentés sur le type Human, et une méthode fly est implémentée directement sur Human.

Lorsque nous appelons fly sur une instance de Human, le compilateur se réfère par défaut à la méthode qui est directement implémentée sur le type, comme montré dans la Liste 19-17.

Nom de fichier : src/main.rs

fn main() {
    let person = Human;
    person.fly();
}

Liste 19-17 : Appel de fly sur une instance de Human

Exécuter ce code imprimera *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 laquelle des méthodes fly nous voulons. La Liste 19-18 montre cette syntaxe.

Nom de fichier : src/main.rs

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}

Liste 19-18 : Spécification de laquelle des méthodes fly du trait nous voulons appeler

Spécifier le nom du trait avant le nom de la méthode indique à Rust laquelle des implémentations de fly nous voulons appeler. Nous pourrions également écrire Human::fly(&person), qui est équivalent à person.fly() que nous avons utilisé dans la Liste 19-18, mais cela est un peu plus long à écrire si nous n'avons pas besoin de faire la distinction.

Exécuter ce code imprime ce qui suit :

This is your captain speaking.
Up!
*waving arms furiously*

Comme la méthode fly prend un paramètre self, si nous avions deux types qui implémentent tous deux un même trait, Rust pourrait déterminer laquelle des implémentations d'un trait utiliser en fonction du 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 a plusieurs types ou traits qui définissent des fonctions non méthodes avec le même nom de fonction, Rust ne sait pas toujours laquelle vous voulez utiliser à moins que vous n'utilisiez une syntaxe entièrement qualifiée. Par exemple, dans la Liste 19-19, nous créons un trait pour un refuge animalier qui veut nommer tous les bébés chiens Spot. Nous créons un trait Animal avec une fonction associée non méthodique baby_name. Le trait Animal est implémenté pour la structure Dog, sur laquelle nous fournissons également directement une fonction associée non méthodique baby_name.

Nom de fichier : 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());
}

Liste 19-19 : Un trait avec une fonction associée et un type avec une fonction associée de même nom qui implémente également le trait

Nous implémentons le code pour nommer tous les bébés chiens Spot dans la fonction associée baby_name définie sur Dog. Le type Dog implémente également le trait Animal, qui décrit les caractéristiques que tous les animaux ont. Les bébés chiens sont appelés 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 directement la fonction associée définie sur Dog. Ce code imprime ce qui suit :

A baby dog is called a Spot

Ce résultat n'est pas celui que nous voulions. Nous voulons appeler la fonction baby_name qui est partie du trait Animal que nous avons implémenté sur Dog pour que le code imprime A baby dog is called a puppy. La technique de spécification du nom du trait que nous avons utilisée dans la Liste 19-18 ne sert pas ici ; si nous modifions main pour le code de la Liste 19-20, nous obtiendrons une erreur de compilation.

Nom de fichier : src/main.rs

fn main() {
    println!("A baby dog is called a {}", Animal::baby_name());
}

Liste 19-20 : Tentative d'appel de la fonction baby_name du trait Animal, mais Rust ne sait pas laquelle des implémentations utiliser

Car Animal::baby_name n'a pas de paramètre self, et il pourrait y avoir d'autres types qui implémentent le trait Animal, Rust ne peut pas déterminer laquelle des implémentations de Animal::baby_name nous voulons. Nous obtiendrons cette erreur du compilateur :

error[E0283]: type annotations needed
  --> src/main.rs:20:43
   |
20 |     println!("A baby dog is called a {}", Animal::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^ cannot infer
type
   |
   = note: cannot satisfy `_: Animal`

Pour faire la distinction et dire à Rust que nous voulons utiliser l'implémentation de Animal pour Dog par opposition à l'implémentation de Animal pour un autre type, nous devons utiliser une syntaxe entièrement qualifiée. La Liste 19-21 montre comment utiliser une syntaxe entièrement qualifiée.

Nom de fichier : src/main.rs

fn main() {
    println!(
        "A baby dog is called a {}",
        <Dog as Animal>::baby_name()
    );
}

Liste 19-21 : Utilisation d'une syntaxe entièrement qualifiée pour spécifier que nous voulons appeler la fonction baby_name du trait Animal telle qu'elle est implémentée sur Dog

Nous fournissons à Rust une annotation de type à l'intérieur des chevrons, ce qui indique que nous voulons appeler la méthode baby_name du trait Animal telle qu'elle est implémentée sur Dog en disant que nous voulons considérer le type Dog comme un Animal pour cet appel de fonction. Ce code imprimera maintenant ce que nous voulons :

A baby dog is called a puppy

En général, la syntaxe entièrement 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 pouvez utiliser une syntaxe entièrement 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éterminer à partir d'autres informations dans le programme. Vous n'avez besoin d'utiliser cette syntaxe plus verbeuse que dans les cas où il y a plusieurs implémentations qui utilisent le même nom et que Rust a besoin d'aide pour identifier laquelle des implémentations vous voulez appeler.

Utilisation de supertraits

Parfois, vous pouvez é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 également le second trait. Vous le feriez pour que la définition de votre trait puisse utiliser les éléments associés du second trait. Le trait dont dépend la définition de votre trait 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 imprimera une valeur donnée formatée de manière à être encadrée d'étoiles. C'est-à-dire que, étant donné une structure Point qui implémente le trait Display de la bibliothèque standard pour obtenir (x, y), lorsque nous appelons outline_print sur une instance de Point qui a 1 pour x et 3 pour y, elle devrait imprimer 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 également Display et fournissent la fonctionnalité dont OutlinePrint a besoin. Nous pouvons le faire dans la définition du trait en spécifiant OutlinePrint: Display. Cette technique est similaire à l'ajout d'une contrainte de trait au trait. La Liste 19-22 montre une implémentation du trait OutlinePrint.

Nom de fichier : 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));
    }
}

Liste 19-22 : Implémentation du trait OutlinePrint qui nécessite la fonctionnalité de Display

Comme nous avons spécifié que 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 deux-points et en spécifiant le trait Display après le nom du trait, nous obtiendrions une erreur indiquant 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, tel que la structure Point :

Nom de fichier : src/main.rs

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

impl OutlinePrint for Point {}

Nous obtenons une erreur indiquant que Display est requis mais non implémenté :

error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:20:6
   |
20 | impl OutlinePrint for Point {}
   |      ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Point`
   = note: in format strings you may be able to use `{:?}` (or {:#?} for
pretty-print) instead
note: required by a bound in `OutlinePrint`
  --> src/main.rs:3:21
   |
3  | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint`

Pour corriger ceci, nous implémentons Display sur Point et satisfaisons la contrainte que OutlinePrint exige, comme ceci :

Nom de fichier : src/main.rs

use std::fmt;

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

Ensuite, l'implémentation du trait OutlinePrint sur Point compilera avec succès, et nous pouvons appeler outline_print sur une instance de Point pour l'afficher dans un contour d'étoiles.

Utilisation du nouveau modèle de type pour implémenter des traits externes

Dans "Implémentation d'un trait sur un type", 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 le trait ou le type, ou les deux, sont locaux à notre crate. Il est possible de contourner cette restriction en utilisant le nouveau modèle de type, qui consiste à créer un nouveau type dans une structure tuple. (Nous avons abordé les structures tuple dans "Utilisation de structures tuple sans champs nommés pour créer différents types".) La structure tuple aura un seul champ et sera une enveloppe mince autour du type pour lequel nous voulons implémenter un trait. Ensuite, le type d'enveloppe est local à notre crate, et nous pouvons implémenter le trait sur l'enveloppe. Nouveau type est un terme qui vient du langage de programmation Haskell. Il n'y a pas de pénalité de performance à l'exécution pour utiliser ce modèle, et le type d'enveloppe est éliminé à la compilation.

Par 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> ; puis nous pouvons implémenter Display sur Wrapper et utiliser la valeur de Vec<T>, comme montré dans la Liste 19-23.

Nom de fichier : 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}");
}

Liste 19-23 : Création d'un type Wrapper autour de Vec<String> pour implémenter 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'index 0 dans la tuple. Ensuite, nous pouvons utiliser la fonctionnalité du type Display sur Wrapper.

Le inconvénient d'utiliser cette technique est que Wrapper est un nouveau type, donc il n'a 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 renvoyer le type interne serait une solution (nous avons discuté de l'implémentation du trait Deref dans "Traiter des pointeurs intelligents comme des références normales avec Deref"). 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 seulement les méthodes que nous voulons manuellement.

Ce nouveau modèle de type est également utile même lorsqu'aucun trait n'est impliqué. Passons à la question et examinons certaines façons avancées d'interagir avec le système de types de Rust.

Sommaire

Félicitations ! Vous avez terminé le laboratoire sur les traits avancés. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.