Les traits avancés
Nous avons vu les traits dans une section du chapitre 10, mais nous n'avons pas abordé certains détails plus avancés. Maintenant que vous en savez plus sur Rust, nous pouvons attaquer les choses sérieuses.
Placer des types à remplacer dans les définitions des traits grâce aux types associés
Les types associés connectent un type à remplacer avec un trait afin que la définition des méthodes puisse utiliser ces types à remplacer dans leur signature. Celui qui implémente un trait doit renseigner un type concret pour être utilisé à la place du type à remplacer pour cette implémentation précise. Ainsi, nous pouvons définir un trait qui utilise certains types sans avoir besoin de savoir exactement quels sont ces types jusqu'à ce que ce trait soit implémenté.
Nous avions dit que vous auriez rarement besoin de la plupart des fonctionnalités avancées de ce chapitre. Les types associés sont un entre-deux : ils sont utilisés plus rarement que les fonctionnalités expliquées dans le reste de ce livre, mais on les rencontre plus fréquemment que la plupart des autres fonctionnalités présentées dans ce chapitre.
Un exemple de trait avec un type associé est le trait Iterator
que fournit la
bibliothèque standard. Le type associé Item
permet de renseigner le type des
valeurs que le type qui implémente le trait Iterator
parcourt. Dans une
section du chapitre 13, nous avions mentionné que la définition du trait
Iterator
ressemblait à cet encart 19-12.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Le type Item
est un type à remplacer, et la définition de la méthode next
informe qu'elle va retourner des valeurs du type Option<Self::Item>
. Ceux qui
implémenterons le trait Iterator
devront renseigner un type concret pour
Item
, et la méthode next
va retourner une Option
qui contiendra une
valeur de ce type concret.
Les types associés ressemblent au même concept que les génériques, car ces derniers nous permettent de définir une fonction sans avoir à renseigner les types avec lesquels elle travaille. Donc pourquoi utiliser les types associés ?
Examinons les différences entre les deux concepts grâce à un exemple du
chapitre 13 qui implémente le trait Iterator
sur la structure Compteur
.
Dans l'encart 13-21, nous avions renseigné que le type Item
était u32
:
Fichier : src/lib.rs
struct Compteur {
compteur: u32,
}
impl Compteur {
fn new() -> Compteur {
Compteur { compteur: 0 }
}
}
impl Iterator for Compteur {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// -- partie masquée ici --
if self.compteur < 5 {
self.compteur += 1;
Some(self.compteur)
} else {
None
}
}
}
Cette syntaxe ressemble aux génériques. Donc pourquoi ne pas simplement définir le
trait Iterator
avec les génériques, comme dans l'encart 19-13 ?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
La différence est que lorsque on utilise les génériques, comme dans l'encart
19-13, on doit annoter les types dans chaque implémentation ; et comme nous
pouvons aussi implémenter Iterator<String> for Compteur
ou tout autre type,
nous pourrions alors avoir plusieurs implémentations de Iterator
pour
Compteur
. Autrement dit, lorsqu'un trait a un paramètre générique, il peut
être implémenté sur un type plusieurs fois, en changeant à chaque fois le type
concret du paramètre de type générique. Lorsque nous utilisons la méthode next
sur Compteur
, nous devons appliquer une annotation de type pour indiquer
quelle implémentation de Iterator
nous souhaitons utiliser.
Avec les types associés, nous n'avons pas besoin d'annoter les types car nous
ne pouvons pas implémenter un trait plusieurs fois sur un même type. Dans l'encart
19-12 qui contient la définition qui utilise les types associés, nous ne pouvons
choisir quel sera le type de Item
qu'une seule fois, car il ne peut
y avoir qu'un seul impl Iterator for Compteur
. Nous n'avons pas à préciser
que nous souhaitons avoir un itérateur de valeurs u32
à chaque fois que nous
faisons appel à next
sur Compteur
.
Les paramètres de types génériques par défaut et la surcharge d'opérateur
Lorsque nous utilisons les paramètres de types génériques, nous pouvons
renseigner un type concret par défaut pour le type générique. Cela évite de
contraindre ceux qui implémentent ce trait d'avoir à renseigner un type concret
si celui par défaut fonctionne bien. La syntaxe pour renseigner un type par
défaut pour un type générique est <TypeARemplacer=TypeConcret>
lorsque nous
déclarons le type générique.
Un bon exemple d'une situation pour laquelle cette technique est utile est avec
la surcharge d'opérateurs. La surcharge d'opérateur permet de personnaliser
le comportement d'un opérateur (comme +
) dans des cas particuliers.
Rust ne vous permet pas de créer vos propres opérateurs ou de surcharger des
opérateurs. Mais vous pouvez surcharger les opérations et les traits listés
dans std::ops
en implémentant les traits associés à l'opérateur. Par exemple,
dans l'encart 19-14 nous surchargeons l'opérateur +
pour additionner ensemble
deux instances de Point
. Nous pouvons faire cela en implémentant le trait
Add
sur une structure Point
:
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 } ); }
La méthode add
ajoute les valeurs x
de deux instances de Point
ainsi que
les valeurs y
de deux instances de Point
pour créer un nouveau Point
. Le
trait Add
a un type associé Output
qui détermine le type retourné pour la
méthode add
.
Le type générique par défaut dans ce code est 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 être familier : un trait avec une méthode et un type
associé. La nouvelle partie concerne Rhs=Self
: cette syntaxe s'appelle les
paramètres de types par défaut. Le paramètre de type générique Rhs
(c'est le raccourci de “Right Hand Side”) qui définit le type du paramètre
rhs
dans la méthode add
. Si nous ne renseignons 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
sur Point
, nous avons utilisé la valeur
par défaut de Rhs
car nous voulions additionner deux instances de Point
.
Voyons un exemple d'implémentation du trait Add
dans lequel nous souhaitons
personnaliser le type Rhs
plutôt que d'utiliser celui par défaut.
Nous avons deux structures, Millimetres
et Metres
, qui stockent des valeurs
dans différentes unités. Ce léger enrobage d'un type existant dans une autre
structure s'appelle le motif newtype, que nous décrivons plus en détail dans
la section Utiliser le motif newtype pour la sécurité et l'abstraction des
types. Nous voulons pouvoir additionner les valeurs en
millimètres avec les valeurs en mètres et appliquer l'implémentation de Add
pour pouvoir faire la conversion correctement. Nous pouvons implémenter Add
sur Millimetres
avec Metres
comme étant le Rhs
, comme dans l'encart
19-15.
Fichier : src/lib.rs
use std::ops::Add;
struct Millimetres(u32);
struct Metres(u32);
impl Add<Metres> for Millimetres {
type Output = Millimetres;
fn add(self, other: Metres) -> Millimetres {
Millimetres(self.0 + (other.0 * 1000))
}
}
Pour additionner Millimetres
et Metres
, nous renseignons
impl Add<Metres>
pour régler la valeur du paramètre de type Rhs
au lieu
d'utiliser la valeur par défaut Self
.
Vous utiliserez les paramètres de types par défaut dans deux principaux cas :
- Pour étendre un type sans casser le code existant
- Pour permettre la personnalisation dans des cas spécifiques que la plupart des utilisateurs n'auront pas
Le trait Add
de la bibliothèque standard est un exemple du second cas :
généralement, vous additionnez deux types similaires, mais le trait Add
offre
la possibilité de personnaliser cela. L'utilisation d'un paramètre de type par
défaut dans la définition du trait Add
signifie que vous n'aurez pas à
renseigner de paramètre en plus la plupart du temps. Autrement dit, il n'est
pas nécessaire d'avoir recours à des assemblages de code, ce qui facilite
l'utilisation du trait.
Le premier cas est similaire au second mais dans le cas inverse : si vous souhaitez ajouter un paramètre de type à un trait existant, vous pouvez lui en donner un par défaut pour permettre l'ajout des fonctionnalités du trait sans casser l'implémentation actuelle du code.
La syntaxe totalement définie pour clarifier les appels à des méthodes qui ont le même nom
Il n'y a rien en Rust qui empêche un trait d'avoir une méthode portant le même nom qu'une autre méthode d'un autre trait, ni ne vous empêche d'implémenter ces deux traits sur un même type. Il est aussi possible d'implémenter directement une méthode avec le même nom que celle présente dans les traits sur ce type.
Lorsque nous faisons appel à des méthodes qui ont un conflit de nom, vous devez
préciser à Rust précisément celle que vous souhaitez utiliser. Imaginons le
code dans l'encart 19-16 dans lequel nous avons défini deux traits, Pilote
et
Magicien
, qui ont tous les deux une méthode voler
. Nous
implémentons ensuite ces deux traits sur un type Humain
qui a déjà lui-aussi
une méthode voler
qui lui a été implémentée. Chaque méthode voler
fait
quelque chose de différent.
Fichier : src/main.rs
trait Pilote { fn voler(&self); } trait Magicien { fn voler(&self); } struct Humain; impl Pilote for Humain { fn voler(&self) { println!("Ici le capitaine qui vous parle."); } } impl Magicien for Humain { fn voler(&self) { println!("Décollage !"); } } impl Humain { fn voler(&self) { println!("*agite frénétiquement ses bras*"); } } fn main() {}
Lorsque nous utilisons voler
sur une instance de Humain
, le compilateur
fait appel par défaut à la méthode qui est directement implémentée sur le type,
comme le montre l'encart 19-17.
Fichier : src/main.rs
trait Pilote { fn voler(&self); } trait Magicien { fn voler(&self); } struct Humain; impl Pilote for Humain { fn voler(&self) { println!("Ici le capitaine qui vous parle."); } } impl Magicien for Humain { fn voler(&self) { println!("Décollage !"); } } impl Humain { fn voler(&self) { println!("*agite frénétiquement ses bras*"); } } fn main() { let une_personne = Humain; une_personne.voler(); }
L'exécution de ce code va afficher *agite frénétiquement ses bras*
, ce qui
démontre que Rust a appelé la méthode voler
implémentée directement sur
Humain
.
Pour faire appel aux méthodes voler
des traits Pilote
ou Magicien
, nous
devons utiliser une syntaxe plus explicite pour préciser quelle méthode voler
nous souhaitons utiliser. L'encart 19-18 montre cette syntaxe.
Fichier : src/main.rs
trait Pilote { fn voler(&self); } trait Magicien { fn voler(&self); } struct Humain; impl Pilote for Humain { fn voler(&self) { println!("Ici le capitaine qui vous parle."); } } impl Magicien for Humain { fn voler(&self) { println!("Décollage !"); } } impl Humain { fn voler(&self) { println!("*agite frénétiquement ses bras*"); } } fn main() { let une_personne = Humain; Pilote::voler(&une_personne); Magicien::voler(&une_personne); une_personne.voler(); }
Si on renseigne le nom du trait avant le nom de la méthode, cela indique à Rust
quelle implémentation de voler
nous souhaitons utiliser. Nous pouvons aussi
écrire Humain::voler(&une_personne)
, qui est équivalent à
une_personne.voler()
que nous avons utilisé dans l'encart 19-18, mais c'est
un peu plus long à écrire si nous n'avons pas besoin de préciser les choses.
L'exécution de ce code affiche ceci :
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
Ici le capitaine qui vous parle.
Décollage !
*agite frénétiquement ses bras*
Comme la méthode voler
prend un paramètre self
, si nous avions deux
types qui implémentaient chacun un des deux traits, Rust pourrait en
déduire quelle implémentation de quel 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 qui ne sont pas des méthodes et qui ont le même nom de fonction, Rust
ne peut pas toujours savoir quel type vous sous-entendez jusqu'à ce que vous
utilisiez la syntaxe totalement définie. Par exemple, le trait Animal
de
l'encart 19-19 a une fonction associée nom_bebe
qui n'est pas une méthode, et
le trait Animal
est implémenté pour la structure Dog
.Il y a aussi une
fonction associée nom_bebe
qui n'est pas une méthode et qui est définie
directement sur Chien
.
Fichier : src/main.rs
trait Animal { fn nom_bebe() -> String; } struct Chien; impl Chien { fn nom_bebe() -> String { String::from("Spot") } } impl Animal for Chien { fn nom_bebe() -> String { String::from("chiot") } } fn main() { println!("Un bébé chien s'appelle un {}", Chien::nom_bebe()); }
Ce code a été conçu pour un refuge pour animaux qui souhaite que tous leurs chiots
soient nommés Spot, ce qui est implémenté dans la fonction associée nom_bebe
de Chien
. Le type Chien
implémente lui aussi le trait Animal
, qui décrit
les caractéristiques que tous les animaux doivent avoir. Les bébés chiens
doivent s'appeler des chiots, et ceci est exprimé dans l'implémentation du
trait Animal
sur Chien
dans la fonction nom_bebe
associée au trait
Animal
.
Dans le main
, nous faisons appel à la fonction Chien::nom_bebe
, qui fait
appel à la fonction associée directement définie sur Chien
. Ce code affiche
ceci :
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
Un bébé chien s'appelle un Spot
Ce résultat n'est pas celui que nous souhaitons. Nous voulons appeler la
fonction nom_bebe
qui fait partie du trait Animal
que nous avons implémenté
sur Chien
afin que le code affiche Un bébé chien s'appelle un chiot
. La
technique pour préciser le nom du trait que nous avons utilisée précédemment ne
va pas nous aider ici ; si nous changeons le main
par le code de l'encart
19-20, nous allons avoir une erreur de compilation.
Fichier : src/main.rs
trait Animal {
fn nom_bebe() -> String;
}
struct Chien;
impl Chien {
fn nom_bebe() -> String {
String::from("Spot")
}
}
impl Animal for Chien {
fn nom_bebe() -> String {
String::from("chiot")
}
}
fn main() {
println!("Un bébé chien s'appelle un {}", Animal::nom_bebe());
}
Comme Animal::nom_bebe
n'a pas de paramètre self
, et qu'il peut y avoir
d'autres types qui implémentent le trait Animal
, Rust ne peut pas savoir
quelle implémentation de Animal::nom_bebe
nous souhaitons utiliser. Nous
obtenons alors cette erreur de compilation :
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0283]: type annotations needed
--> src/main.rs:20:43
|
20 | println!("Un bébé chien s'appelle un {}", Animal::nom_bebe());
| ^^^^^^^^^^^^^^^^ cannot infer type
|
= note: cannot satisfy `_: Animal`
For more information about this error, try `rustc --explain E0283`.
error: could not compile `traits-example` due to previous error
Pour expliquer à Rust que nous souhaitons utiliser l'implémentation de Animal
pour Chien
et non pas l'implémentation de Animal
pour d'autres types, nous
devons utiliser la syntaxe totalement définie. L'encart 19-21 montre comment
utiliser la syntaxe totalement définie.
Fichier : src/main.rs
trait Animal { fn nom_bebe() -> String; } struct Chien; impl Chien { fn nom_bebe() -> String { String::from("Spot") } } impl Animal for Chien { fn nom_bebe() -> String { String::from("chiot") } } fn main() { println!("Un bébé chien s'appelle un {}", <Chien as Animal>::nom_bebe()); }
Nous avons donné à Rust une annotation de type entre des chevrons, ce qui
indique que nous souhaitons appeler la méthode nom_bebe
du trait Animal
telle qu'elle est implémentée sur Chien
en indiquant que nous souhaitons traiter
le type Chien
comme étant un Animal
pour cet appel de fonction. Ce code va
désormais afficher ce que nous souhaitons :
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
Un bébé chien s'appelle un chiot
De manière générale, une syntaxe totalement définie est définie comme ceci :
<Type as Trait>::function(destinataire_si_methode, argument_suivant, ...);
Pour les fonctions associées qui ne sont pas des méthodes, il n'y a pas de
destinataire
: il n'y a qu'une liste d'arguments. Vous pouvez utiliser la
syntaxe totalement définie à n'importe quel endroit où vous faites appel à des
fonctions ou des méthodes. Cependant, vous avez la possibilité de ne pas
renseigner toute partie de cette syntaxe que Rust peut déduire à partir
d'autres informations présentes dans le code. Vous avez seulement besoin
d'utiliser cette syntaxe plus verbeuse dans les cas où il y a plusieurs
implémentations qui utilisent le même nom et que Rust doit être aidé pour
identifier quelle implémentation vous souhaitez appeler.
Utiliser les supertraits pour utiliser la fonctionnalité d'un trait dans un autre trait
Des fois, vous pourriez avoir besoin d'un trait pour utiliser la fonctionnalité d'un autre trait. Dans ce cas, vous devez pouvoir compter sur le fait que le trait dépendant soit bien implémenté. Le trait sur lequel vous comptez est alors un supertrait du trait que vous implémentez.
Par exemple, imaginons que nous souhaitons créer un trait OutlinePrint
qui
offre une méthode outline_print
affichant une valeur entourée d'astérisques.
Ainsi, pour une structure Point
qui implémente Display
pour afficher (x, y)
,
lorsque nous faisons appel à outline_print
sur une instance de Point
qui a
1
pour valeur de x
et 3
pour y
, cela devrait afficher ceci :
**********
* *
* (1, 3) *
* *
**********
Dans l'implémentation de outline_print
, nous souhaitons utiliser la
fonctionnalité du trait Display
. De ce fait, nous devons indiquer que le
trait OutlinePrint
fonctionnera uniquement pour les types qui auront également
implémenté Display
et qui fourniront la fonctionnalité dont a besoin
OutlinePrint
. Nous pouvons faire ceci dans la définition du trait en
renseignant OutlinePrint: Display
. Cette technique ressemble à l'ajout d'un
trait lié au trait. L'encart 19-22 montre une implémentation du trait
OutlinePrint
.
Fichier : src/main.rs
use std::fmt; trait OutlinePrint: fmt::Display { fn outline_print(&self) { let valeur = self.to_string(); let largeur = valeur.len(); println!("{}", "*".repeat(largeur + 4)); println!("*{}*", " ".repeat(largeur + 2)); println!("* {} *", valeur); println!("*{}*", " ".repeat(largeur + 2)); println!("{}", "*".repeat(largeur + 4)); } } fn main() {}
Comme nous avons précisé que OutlinePrint
nécessite le trait Display
, nous
pouvons utiliser la fonction to_string
qui est automatiquement implémentée
pour n'importe quel type qui implémente Display
. Si nous avions essayé
d'utiliser to_string
sans ajouter un double-point et en renseignant le trait
Display
après le nom du trait, nous aurions alors obtenu une erreur qui nous
informerait qu'il n'y a pas de méthode to_string
pour le type &Self
dans la
portée courante.
Voyons ce qui ce passe lorsque nous essayons d'implémenter OutlinePrint
sur
un type qui n'implémente pas Display
, comme c'est le cas de la structure
Point
:
Fichier : src/main.rs
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let valeur = self.to_string();
let largeur = valeur.len();
println!("{}", "*".repeat(largeur + 4));
println!("*{}*", " ".repeat(largeur + 2));
println!("* {} *", valeur);
println!("*{}*", " ".repeat(largeur + 2));
println!("{}", "*".repeat(largeur + 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 qui dit que Display
est nécessaire mais n'est pas
implémenté :
$ 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: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`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` due to previous error
Pour régler cela, nous implémentons Display
sur Point
afin de répondre aux
besoins de OutlinePrint
, comme ceci :
Fichier : src/main.rs
trait OutlinePrint: fmt::Display { fn outline_print(&self) { let valeur = self.to_string(); let largeur = valeur.len(); println!("{}", "*".repeat(largeur + 4)); println!("*{}*", " ".repeat(largeur + 2)); println!("* {} *", valeur); println!("*{}*", " ".repeat(largeur + 2)); println!("{}", "*".repeat(largeur + 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(); }
Ceci fait, l'implémentation du trait OutlinePrint
sur Point
va se
compiler avec succès, et nous pourrons appeler outline_print
sur une instance
de Point
pour l'afficher dans le cadre constitué d'astérisques.
Utiliser le motif newtype pour implémenter des traits externes sur des types externes
Dans une section du chapitre 10, nous avions mentionné la règle de l'orphelin qui énonçait que nous pouvions implémenter un trait sur un type à condition que le trait ou le type soit local à notre crate. Il est possible de contourner cette restriction en utilisant le motif newtype, ce qui implique de créer un nouveau type dans une structure tuple (nous avons vu les structures tuple dans la section “Utilisation de structures tuples sans champ nommé pour créer des types différents” du chapitre 5). La structure tuple aura un champ et sera une petite enveloppe pour le type sur lequel nous souhaitons implémenter le trait. Ensuite, le type enveloppant est local à notre crate, et nous pouvons lui implémenter un trait. Newtype est un terme qui provient du langage de programmation Haskell. Il n'y a pas de conséquence sur les performance à l'exécution pour l'utilisation de ce motif, ce qui signifie que le type enveloppant est résolu à la compilation.
Comme exemple, disons que nous souhaitons 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 construire une structure Enveloppe
qui possède une instance de
Vec<T>
; et ensuite nous pouvons implémenter Display
sur Enveloppe
et
utiliser la valeur Vec<T>
, comme dans l'encart 19-23.
Fichier : src/main.rs
use std::fmt; struct Enveloppe(Vec<String>); impl fmt::Display for Enveloppe { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } } fn main() { let w = Enveloppe(vec![String::from("hello"), String::from("world")]); println!("w = {}", w); }
L'implémentation de Display
utilise self.0
pour accéder à la valeur de
Vec<T>
, car Enveloppe
est une structure tuple et Vec<T>
est l'élément à
l'indice 0 du tuple. Ensuite, nous pouvons utiliser la fonctionnalité du type
Display
sur Enveloppe
.
Le désavantage d'utiliser cette technique est que Enveloppe
est un nouveau
type, donc il n'implémente pas toutes les méthodes de la valeur qu'il possède.
Il faudrait implémenter toutes les méthodes de Vec<T>
directement sur
Enveloppe
de façon à ce qu'elles délèguent aux méthodes correspondantes de
self.0
, ce qui nous permettrait d'utiliser Enveloppe
exactement comme un
Vec<T>
. Si nous voulions que le nouveau type ait toutes les méthodes du type
qu'il possède, l'implémentation du trait Deref
(que nous avons vu dans
une section du chapitre 15) sur
Enveloppe
pour retourner le type interne pourrait être une solution. Si nous
ne souhaitons pas que le type Enveloppe
ait toutes les méthodes du type qu'il
possède (par exemple, pour limiter les fonctionnalités du type Enveloppe
),
nous n'avons qu'à implémenter manuellement que les méthodes que nous souhaitons.
Maintenant vous savez comment le motif newtype est utilisé en lien avec les traits ; c'est aussi un motif très utile même lorsque les traits ne sont pas concernés. Changeons de sujet et découvrons d'autres techniques avancées pour interagir avec le système de type de Rust.