12. La généricité▲
Comme son nom l'indique, cette section abordera les types et fonctionnalités génériques. La généricité peut être très utile pour réduire les répétitions au sein du code dans de nombreux cas, mais vous demandera, en échange, d'apporter quelques précisions supplémentaires à propos de la syntaxe. Notez également que rendre une ressource générique signifie que n'importe quelle ressource sera traitée de la même manière, il est nécessaire de savoir quels types de ressources peuvent être réellement traités, dans les cas où il est nécessaire de le spécifier.
La généricité est principalement utilisée pour rendre générique un, ou plusieurs, paramètre passé à une fonction. Par convention, un paramètre générique doit avoir un identificateur respectant la convention de nommage CamelCase et être déclaré entre un chevron ouvrant (<) et un chevron fermant(>) : <Aaa, Bbb, ...>, qui est souvent représenté par le paramètre <T>. En déclarant un paramètre générique de type <T>, on accepte de recevoir un, ou plusieurs, paramètre de ce type. Tout paramètre déclaré comme générique est générique, tout le reste est concret (non-générique).
Par exemple, voici une fonction générique nommée foo qui prend un paramètre de type T (de n'importe quel type, donc) :
fn
foo<T>(p: T) { ... }
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
// A est un type concret.
struct
A;
// Lorsque nous déclarons `Single`, la première occurrence de `A` n'est
// pas précédée du type générique `<A>`. Le type `Single` et `A` sont donc
// concrets.
struct
Single(A);
// ^ Voici la première occurrence du type `A`.
// En revanche, ici, `<T>` précède la première occurrence `T`, donc le type
// `SingleGen` est générique. Puisque le type `T` est générique, cela pourrait être
// "n'importe quoi", y compris le type concret `A` déclaré au début du fichier.
struct
SingleGen<T>(T);
fn
main() {
// `Single` est un type concret et prend explicitement un paramètre
// de type `A`.
let
_s = Single(A);
// On créé une variable nommée `_char` de type `SingleGen<char>`
// et on lui assigne la valeur `SingleGen('a')`.
// Le type requis du paramètre passé pour cette instance de `SingleGen`
// est spécifié, mais il peut être omis, exemple ---
let
_char: SingleGen<char> = SingleGen('a'
);
// --->
let
_t = SingleGen(A); // On passe une instance
// du type `A` définit en haut.
let
_i32 = SingleGen(6
); // On passe un entier de type `i32`.
let
_char = SingleGen('a'
); // On passe un `char`.
}
Voir aussi
12-1. Les fonctions▲
Les règles précédemment présentées s'appliquent également aux fonctions : un type T est générique lorsqu'il est précédé par la déclaration <T> (La lettre varie bien entendu selon le nom du type générique que vous donnez).
Lorsque vous utilisez des fonctions génériques il peut, parfois, être nécessaire d'expliciter le type des paramètres dans le cas où, par exemple, la fonction appelée possède un type de renvoi générique, ou encore si le compilateur ne dispose pas d'assez d'informations pour inférer le type des paramètres.
Une fonction dont le type des paramètres est explicité devrait ressembler à ceci : fn
::<A, B, ...>().
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
struct
A; // Le type `A` est concret.
struct
S(A); // Le type `S` est concret.
struct
SGen<T>(T); // Le type `SGen` est générique.
// Toutes les fonctions qui suivront possèdent les paramètres
// qui leur sont passés et libèrent la mémoire aussitôt.
// On définit une fonction nommée `reg_fn` qui prend un argument `_s` de type
// `S`. Ce dernier n'est pas précédé d'un `<T>` (ou, en l'occurrence un `<S>`)
// donc le type n'est pas générique.
fn
reg_fn(_s: S) {}
// On définit une fonction nommée `gen_spec_t` qui prend un argument `_s` de type
// `SGen<T>`.
// Le type d'argument imposé à la structure SGen<T> est précisé, mais puisque
// `A` n'est pas précédé par le type générique `<A>`, le type n'est pas générique.
fn
gen_spec_t(_s: SGen<A>) {}
// ^ il aurait fallu déclarer `A` comme générique: `<A>`.
// On définit une fonction nommée `gen_spec_i32` qui prend un argument `_s` de
// type `SGen<i32>`.
// Le type de paramètre supporté par la structure SGen a été spécifié, `i32`.
// Puisque `i32` n'est pas un type générique, cette fonction n'est pas générique non plus.
fn
gen_spec_i32(_s: SGen<i32>) {}
// On définit une fonction nommée `generic` qui prend un paramètre `_s` de type
// `SGen<T>`.
// Puisque `SGen<T>` est précédé par le type générique `<T>`,
// cette fonction est donc générique.
fn
generic<T>(_s: SGen<T>) {}
fn
main() {
// Appel des fonctions qui ne sont pas génériques.
reg_fn(S(A)); // On passe en paramètre un type concret.
gen_spec_t(SGen(A)); // Type `A` implicitement spécifié car la fonction
// n'accepte que la signature `SGen<A>`.
gen_spec_i32(SGen(6
)); // Type `i32` implicitement spécifié car la fonction
// n'accepte que la signature `SGen<i32>`.
// Type du paramètre explicitement spécifié pour
// la fonction `generic()`.
// Le type peut être omis car le compilateur peut inférer
// le type à partir du littéral.
generic::<char>(SGen('a'
));
// Type du paramètre implicitement spécifié pour la fonction
// `generic()` car le compilateur est capable d'inférer le type
// à partir du littéral.
generic(SGen('c'
));
}
Voir aussi
12-2. Implémentation générique▲
Tout comme les fonctions, les implémentations nécessitent quelques précisions pour être génériques.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
struct
S; // On déclare le type concret `S`.
struct
GenericVal<T>(T); // On déclare le type générique `GenericVal`.
// Implémentation de GenericVal où nous précisons que cette méthode doit être
// implémentée uniquement pour le type `f32`.
impl
GenericVal<f32> {
fn
say_hello_f32(&
self
) -> (){
println!
("I'm a float! :D"
);
}
} // On spécifie `f32`
impl
GenericVal<S> {
fn
say_hello_s(&
self
) -> (){
println!
("I'm a S object! :D"
);
}
} // On spécifie le type `S` pour les mêmes raisons qu'au-dessus.
// `<T>` doit précéder le type pour le rendre générique.
impl
<T> GenericVal<T> {
fn
say_hello(&
self
) -> (){
println!
("I'm a generic object! :D"
);
}
}
struct
Val {
val: f64
}
struct
GenVal<T>{
gen_val: T
}
// Implémentation de Val.
impl
Val {
fn
value(&
self
) -> &
f64 { &
self
.val }
}
// Implémentation de GenVal pour le type générique `T`.
impl
<T> GenVal<T> {
fn
value(&
self
) -> &
T { &
self
.gen_val }
}
fn
main() {
let
x = Val { val: 3
.0
};
let
y = GenVal { gen_val: 3
i32 };
println!
("
{}
,
{}
"
, x.value(), y.value());
GenericVal(1
.0
).say_hello_f32();
GenericVal(S).say_hello_s();
GenericVal("prout"
).say_hello();
}
Voir aussi
12-3. Les traits▲
Bien entendu, les traits peuvent également être génériques. Dans cette section, nous allons en créer un qui ré-implémente le trait Drop qui proposera une méthode générique qui aura pour fonction de libérer l'instance qui l'appelle ainsi qu'un paramètre passé.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
// Types non-copiables.
struct
Empty;
struct
Null;
// Un trait générique qui reçoit un type générique `T`.
trait
DoubleDrop<T> {
// On déclare une méthode qui sera implémentée par la structure
// appelante (caller) et prendra un paramètre `T` en entrée mais n'en fera rien (juste le libérer).
fn
double_drop(self
, _: T);
}
// On implémente `DoubleDrop<T>` pour n'importe quel paramètre `T` et
// n'importe quelle structure appelante (`U`).
impl
<T, U> DoubleDrop<T> for
U {
// Cette méthode prend "possession" des deux paramètres (`U` et `T`),
// et sont donc tous deux libérés.
fn
double_drop(self
, _: T) {}
}
fn
main() {
let
empty = Empty;
let
null = Null;
// `empty` et `null` sont désalloués.
empty.double_drop(null);
// empty;
// null;
// ^ TODO: Essayez de décommenter ces lignes.
}
Voir aussi
La documentation du trait Drop, le chapitre sur les structures et les traits.
12-4. Les restrictions▲
Lorsque nous travaillons avec la généricité, il est courant d'assigner une « restriction » à un type générique pour spécifier quel trait il doit implémenter. Dans l'exemple suivant, nous utilisons le trait Display pour afficher quelque chose en console, il est alors assigné au type T. Autrement dit, T doit implémenter Display.
2.
3.
4.
5.
// On déclare une fonction nommée `printer` qui prend, en entrée,
// un type générique `T` qui doit implémenter le trait `Display`.
fn
printer<T: Display>(t: T) {
println!
("
{}
"
, t);
}
Les ressources passées en paramètre sont toutes soumises à ces restrictions et doivent forcément remplir les conditions :
2.
3.
4.
struct
S<T: Display>(T);
// Erreur! `Vec<T>` n'implémente pas le trait `Display`.
let
s = S(vec!
[1
]);
Les instances des types génériques peuvent également accéder aux méthodes appartenant au(x) trait(s) présent(s) dans les restrictions du type. Par exemple :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
// Un trait qui implémente le marqueur `{:?}`.
use
std::fmt::Debug;
trait
HasArea {
fn
area(&
self
) -> f64;
}
impl
HasArea for
Rectangle {
fn
area(&
self
) -> f64 { self
.length * self
.height }
}
#[derive(Debug)]
struct
Rectangle { length: f64, height: f64 }
#[allow(dead_code)]
struct
Triangle { length: f64, height: f64 }
// Le type générique `T` doit implémenter le trait `Debug`.
// Qu'importe le type de `T`, cela fonctionnera.
fn
print_debug<T: Debug>(t: &
T) {
println!
("
{:?}
"
, t);
}
// `T` doit implémenter le trait `HasArea`. N'importe quelle
// structure remplissant les conditions d'entrée peut accéder
// à la méthode `area` du trait `HasArea`.
fn
area<T: HasArea>(t: &
T) -> f64 { t.area() }
fn
main() {
let
rectangle = Rectangle { length: 3
.0
, height: 4
.0
};
let
_triangle = Triangle { length: 3
.0
, height: 4
.0
};
print_debug(&
rectangle);
println!
("Area:
{}
"
, area(&
rectangle));
// print_debug(&_triangle);
// println!("Area: {}", area(&_triangle));
// ^ TODO: Essayez de décommenter ces lignes.
// | Erreur: N'implémente pas l'un de ces traits: `Debug` ou `HasArea`.
}
Les conditions d'entrée pour les paramètres génériques peuvent également être spécifiées en utilisant le mot-clé where, les rendant plus explicites, plus lisibles.
Voir aussi
Les traits dédiés à l'affichage et le formatage std::fmt, les structures et les traits.
12-4-1. Exemple d'utilisation : Traits sans services▲
Puisqu'il est possible d'imposer des conditions aux types génériques grâce aux traits, même si ces derniers ne possèdent aucune fonctionnalité (i.e. aucun service, aucune méthode), il est toujours possible de vous en servir comme simple « filtre ». Eq et Ord font partie de ces traits « vides » fournis par la bibliothèque standard.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
struct
Cardinal;
struct
BlueJay;
struct
Turkey;
trait
Red {}
trait
Blue {}
impl
Red for
Cardinal {}
impl
Blue for
BlueJay {}
// Ces fonctions ne prendront en entrée que des ressources
// ayant implémenté les traits `Red` ou `Blue`.
// Le fait que ces derniers soient vides n'a que peu d'importance.
fn
red<T: Red>(_: &
T) -> &
'static
str { "red"
}
fn
blue<T: Blue>(_: &
T) -> &
'static
str { "blue"
}
fn
main() {
let
cardinal = Cardinal;
let
blue_jay = BlueJay;
let
_turkey = Turkey;
// La fonction `red()` ne fonctionnera pas sur une instance
// de la structure `BlueJay`, et vice versa, à cause des
// restrictions imposées par les fonctions (i.e. `red()` et `blue()`).
println!
("A cardinal is
{}
"
, red(&
cardinal));
println!
("A blue jay is
{}
"
, blue(&
blue_jay));
//println!("A turkey is {}", red(&_turkey));
// ^ TODO: Essayez de décommenter cette ligne.
}
Voir aussi
La documentation du trait Eq, la documentation du trait Ord et les traits.
12-5. Restrictions multiples▲
Il est possible d'additionner les conditions grâce à l'opérateur +. Tout comme les types concrets, les types génériques sont séparés par une virgule ,.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
use
std::fmt::{Debug, Display};
fn
compare_prints<T: Debug + Display>(t: &
T) {
println!
("Debug: `
{:?}
`"
, t);
println!
("Display: `
{}
`"
, t);
}
fn
compare_types<T: Debug, U: Debug>(t: &
T, u: &
U) {
println!
("t: `
{:?}
"
, t);
println!
("u: `
{:?}
"
, u);
}
fn
main() {
let
string = "words"
;
let
array = [1
, 2
, 3
];
let
vec = vec!
[1
, 2
, 3
];
compare_prints(&
string);
// compare_prints(&array);
// TODO ^ Essayez de décommenter cette ligne.
compare_types(&
array, &
vec);
}
Voir aussi
12-6. La clause where▲
Une restriction peut également être explicitée par la clause where
. Cette dernière se trouvera alors avant l'accolade ouvrante ({) plutôt qu'à la déclaration du type(e.g. <A: Display, B: Debug, ...>). Avec where
, vous pouvez également ajouter arbitrairement d'autres types en plus de spécifier les traits à implémenter pour les types génériques.
where
peut être utile dans plusieurs cas :
- Lorsque vous ajoutez des restrictions aux types génériques, facilitant la lecture :
2.
3.
4.
5.
6.
7.
impl
<A: TraitB + TraitC, D: TraitE + TraitF> MyTrait<A, D> for
YourType {}
// Les restrictions sont explicitées
// par la condition `where`.
impl
<A, D> MyTrait<A, D> for
YourType where
A: TraitB + TraitC,
D: TraitE + TraitF {}
- Lorsque d'autres types sont ajoutés aux restrictions :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
use
std::fmt::{Debug, Formatter, Result
};
trait
PrintInOption {
fn
print_in_option(self
);
}
// Parce que nous pourrions modifier la restriction sans spécifier le
// conteneur `T: Debug` ou adopter une autre approche,
// il est nécessaire d'utiliser la condition `where`:
impl
<T> PrintInOption for
T where
Option
<T>: Debug{
// Nous spécifions la restriction de type `Option<T>: Debug` parce que
// c'est ce que nous souhaitons afficher. Faire autrement pourrait nous
// induire en erreur quant au type de restriction à spécifier.
fn
print_in_option(self
) {
println!
("
{:?}
"
, Some
(self
));
}
}
fn
main() {
let
vec = vec!
[1
, 2
, 3
];
vec.print_in_option();
}
Voir aussi
RFC pour la condition/clause where, les structures, les traits.
12-7. Les éléments associés▲
Le concept « d'éléments associés » est un ensemble de règles appliquées sur différents types d'éléments. C'est une extension des traits génériques, leur permettant de définir de nouveaux « éléments » en leur sein ; En l'occurrence, ces derniers sont nommés « types associés » et proposent une approche moins fastidieuse pour l'utilisation des patterns (e.g. déclaration des types) lorsqu'un trait
reçoit des paramètres génériques en entrée.
Voir aussi
12-7-1. Le problème▲
Un trait
qui possède des types génériques en entrée doit respecter certaines règles : Les utilisateurs du trait doivent spécifier tous les types de données génériques supportés par le conteneur.
Dans l'exemple ci-dessous, le trait
Contains permet l'utilisation des types génériques A et B. Le trait
est alors implémenté pour le type Container en spécifiant le type i32 pour les deux types génériques (i.e. A et B). Ils peuvent donc être soumis à la fonction fn
difference().
Puisque le type Contains est générique, nous sommes obligés de déclarer tous les types génériques supportés par Contains pour la fonction difference(). En pratique, nous opterions pour une approche nous permettant d'inférer les types génériques supportés par Contains à partir de l'entrée C. C'est ce que nous verrons dans la section suivante, car les types associés autorisent cette approche.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
struct
Container(i32, i32);
// Nous déclarons un trait qui vérifie si deux items sont contenus
// par le conteneur.
// Le conteneur pourra également fournir la première ou dernière
// valeur.
trait
Contains<A, B> {
fn
contains(&
self
, &
A, &
B) -> bool;
fn
first(&
self
) -> i32;
fn
last(&
self
) -> i32;
}
impl
Contains<i32, i32> for
Container {
// Renvoie `true` si les nombres stockés sont égaux.
fn
contains(&
self
, number_1: &
i32, number_2: &
i32) -> bool {
(&
self
.0
== number_1) &&
(&
self
.1
== number_2)
}
// On récupère la première valeur.
fn
first(&
self
) -> i32 { self
.0
}
// On récupère la dernière valeur.
fn
last(&
self
) -> i32 { self
.1
}
}
// `C` contient `A` et `B`. Les déclarer une nouvelle fois dans les paramètres
// génériques de la fonction est fastidieux.
fn
difference<A, B, C>(container: &
C) -> i32 where
C: Contains<A, B> {
container.last() - container.first()
}
fn
main() {
let
number_1 = 3
;
let
number_2 = 10
;
let
container = Container(number_1, number_2);
println!
("Does container contain
{}
and
{}
:
{}
"
,
&
number_1, &
number_2,
container.contains(&
number_1, &
number_2));
println!
("First number:
{}
"
, container.first());
println!
("Last number:
{}
"
, container.last());
println!
("The difference is:
{}
"
, difference(&
container));
}
Voir aussi
12-7-2. Les types associés▲
L'utilisation des « types associés » améliore la lisibilité du code en assignant les types génériques, au sein du trait, comme « types de sortie ». La syntaxe pour les déclarer est la suivante :
2.
3.
4.
5.
6.
7.
8.
9.
10.
// `A` et `B` sont déclarés au sein du trait par le biais du mot-clé `type`.
// Notez toutefois que `type`, dans ce contexte, n'a pas la même fonction
// que le `type` utilisé pour créer des alias.
trait
Contains {
type
A;
type
B;
// La syntaxe a été modifiée pour faire référence aux types génériques.
fn
contains(&
self
, &
Self
::A, &
Self
::B) -> bool;
}
Notez que les fonctions qui utilisent le trait Contains n'ont plus du tout besoin de déclarer les types génériques A et B :
2.
3.
4.
5.
6.
// Sans les types associés.
fn
difference<A, B, C>(container: &
C) -> i32 where
C: Contains<A, B> { ... }
// Avec les types associés.
fn
difference<C: Contains>(container: &
C) -> i32 { ... }
Éditons l'exemple de la section précédente en utilisant les types associés :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
struct
Container(i32, i32);
// Nous déclarons un trait qui vérifie si deux items sont stockés
// dans le conteneur.
// Le conteneur pourra également fournir la première ou dernière valeur.
trait
Contains {
// Nous définissons une bonne fois pour tous les types génériques
// que les méthodes/fonctions pourront utiliser.
type
A;
type
B;
fn
contains(&
self
, &
Self
::A, &
Self
::B) -> bool;
fn
first(&
self
) -> i32;
fn
last(&
self
) -> i32;
}
impl
Contains for
Container {
// On précise le type de `A` et `B`. Si le type d'entrée est
// `Container(i32, i32)`, les types de sorties seront alors
// typés tous les deux `i32`.
type
A = i32; // typé i32
type
B = i32; // typé i32
// `&Self::A` et `&Self::B` sont également valides ici.
// Essayez de remplacer la référence number_1(`&i32`) par `&Self::A`,
// et la référence number_2(`&i32) par `&Self::B`!
fn
contains(&
self
, number_1: &
i32, number_2: &
i32) -> bool {
(&
self
.0
== number_1) &&
(&
self
.1
== number_2)
}
// Récupère la première valeur.
fn
first(&
self
) -> i32 { self
.0
}
// Récupère la dernière valeur.
fn
last(&
self
) -> i32 { self
.1
}
}
fn
difference<C: Contains>(container: &
C) -> i32 {
container.last() - container.first()
}
fn
main() {
let
number_1 = 3
;
let
number_2 = 10
;
let
container = Container(number_1, number_2);
println!
("Does container contain
{}
and
{}
:
{}
"
,
&
number_1, &
number_2,
container.contains(&
number_1, &
number_2));
println!
("First number:
{}
"
, container.first());
println!
("Last number:
{}
"
, container.last());
println!
("The difference is:
{}
"
, difference(&
container));
}
12-8. Les paramètres fantômes▲
Un type de paramètre fantôme n'est pas utilisé à l'exécution, mais est vérifié statiquement (et seulement) au moment de la compilation.
Les types de données peuvent utiliser des types de paramètres génériques supplémentaires pour agir en tant que « marqueurs » ou pour effectuer une vérification du/des type(s) au moment de la compilation. Ces paramètres « supplémentaires » ne stockent aucune ressource et sont inactifs à l'exécution.
Dans l'exemple ci-dessous, nous présentons la structure std::marker::PhantomData avec le concept de « type de paramètre fantôme » pour créer des tuples contenant différents types de données.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
use
std::marker::PhantomData;
// Un tuple qui prend un type générique `A` et un paramètre fantôme `B`.
#[derive(PartialEq)]
// Permet de tester l'égalité entre les instances du type.
struct
PhantomTuple<A, B>(A,PhantomData<B>);
// Un tuple qui prend un type générique `A` et un paramètre fantôme `B`.
#[derive(PartialEq)]
// Permet de tester l'égalité entre les instances du type.
struct
PhantomStruct<A, B> { first: A, phantom: PhantomData<B> }
// Note: De la mémoire sera allouée pour le type générique `A`, mais pas pour `B`.
// En revanche, `B` ne pourra pas être utilisé à l'exécution.
fn
main() {
// Ici, les types `f32` et `f64` sont des paramètres fantômes.
// Types spécifiés pour PhantomTuple: `<char, f32>`.
let
_tuple1: PhantomTuple<char, f32> = PhantomTuple('Q'
, PhantomData);
// Types spécifiés pour PhantomTuple: `<char, f64>`.
let
_tuple2: PhantomTuple<char, f64> = PhantomTuple('Q'
, PhantomData);
// Types spécifiés pour PhantomStruct: `<char, f32>`.
let
_struct1: PhantomStruct<char, f32> = PhantomStruct {
first: 'Q'
,
phantom: PhantomData,
};
// Types spécifiés pour PhantomStruct: `<char, f64>`.
let
_struct2: PhantomStruct<char, f64> = PhantomStruct {
first: 'Q'
,
phantom: PhantomData,
};
// Erreur! Les deux ressources ne peuvent pas être comparées:
// println!("_tuple1 == _tuple2 yields: {}",
// _tuple1 == _tuple2);
// Erreur! Les deux ressources ne peuvent pas être comparées:
// println!("_struct1 == _struct2 yields: {}",
// _struct1 == _struct2);
}
Voir aussi
L'attribut Derive, les structures et les tuples.
12-8-1. Exemple d'utilisation Petite précision▲
Nous allons créer une méthode chargée de calculer dans deux unités de mesure différentes (le pied et le millimètre) et nous implémenterons le trait
Add avec un type générique fantôme. Voici l'implémentation du trait
Add :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
// Cette implémentation devrait imposer: `Self + RHS = Output`
// où `Self` est la valeur par défaut de `RHS` si elle n'est pas spécifiée
// dans l'implémentation.
pub
trait
Add<RHS = Self
> {
type
Output;
fn
add(self
, rhs: RHS) -> Self
::Output;
}
// `Output` doit être de type `T<U>` pour que `T<U> + T<U> = T<U>`.
impl
<U> Add for
T<U> {
type
Output = T<U>;
...
}
Voici l'implémentation complète :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
use
std::ops::Add;
use
std::marker::PhantomData;
/// On créé des énumérations vides pour déclarer le type des
// unités de mesures.
#[derive(Debug, Clone, Copy)]
enum
Inch {}
#[derive(Debug, Clone, Copy)]
enum
Mm {}
///
`Length`
est une structure prenant un type générique fantôme
`Unit`
,
///
`f64`
implémente déjà les traits
`Clone`
et
`Copy`
.
#[derive(Debug, Clone, Copy)]
struct
Length<Unit>(f64, PhantomData<Unit>);
/// Le trait
`Add`
définit le comportement de l'opérateur
`+`
.
impl
<Unit> Add for
Length<Unit> {
type
Output = Length<Unit>;
// La méthode add() renvoie une nouvelle instance de la
// structure `Length` contenant la somme.
fn
add(self
, rhs: Length<Unit>) -> Length<Unit> {
// L'opérateur `+` appelle l'implémentation
// du trait `Add` pour le type `f64`.
Length(self
.0
+ rhs.0
, PhantomData)
}
}
fn
main() {
// On initialise `one_foot` pour avoir un type générique fantôme `Inch`.
// Ce "fantôme" sert de marqueur et classe cette instance dans
// l'unité de mesure "Pied".
let
one_foot: Length<Inch> = Length(12
.0
, PhantomData);
// On initialise `one_meter` pour avoir un type générique fantôme `Mm`.
// Ce "fantôme" sert de marqueur et classe cette instance dans
// l'unité de mesure "Millimètre".
let
one_meter: Length<Mm> = Length(1000
.0
, PhantomData);
// L'opérateur `+` appelle la méthode `add()` que nous avons
// précédemment implémentée pour `Length<Unit>`.
// Maintenant que `Length` implémente le trait `Copy`, `add()` ne
// prend pas possession (ne consomme pas) `one_foot` et `one_meter` mais
// les copie dans `self` et `rhs`.
let
two_feet = one_foot + one_foot;
let
two_meters = one_meter + one_meter;
// L'addition fonctionne.
println!
("one foot + one_foot =
{:?}
in"
, two_feet.0
);
println!
("one meter + one_meter =
{:?}
mm"
, two_meters.0
);
// Les opérations illogiques échouent comme prévu:
// Erreur à la compilation: mismatched type.
// let one_feter = one_foot + one_meter;
}
Voir aussi
Le système d'emprunts, les restrictions, les énumérations, impl et self, la surcharge des opérateurs, le pattern ref, les traits et les tuples.