IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Rust par l'exemple


précédentsommairesuivant

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) :

 
Sélectionnez
fn foo<T>(p: T) { ... }
 
Sélectionnez
1.
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

Les structures

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, ...>().

 
Sélectionnez
1.
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

Les fonctions, les structures.

12-2. Implémentation générique

Tout comme les fonctions, les implémentations nécessitent quelques précisions pour être génériques.

 
Sélectionnez
1.
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: 3i32 };

    println!("{}, {}", x.value(), y.value());
    GenericVal(1.0).say_hello_f32();
    GenericVal(S).say_hello_s();
    GenericVal("prout").say_hello();
}

Voir aussi

Renvoi de références, impl, les structures.

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é.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 ,.

 
Sélectionnez
1.
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

std::fmt, les traits.

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 :
 
Sélectionnez
1.
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 :
 
Sélectionnez
1.
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

La RFC des éléments associés.

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 traitContains 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.

 
Sélectionnez
1.
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

Les structures.

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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 traitAdd avec un type générique fantôme. Voici l'implémentation du traitAdd :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.


précédentsommairesuivant

Licence Creative Commons
Le contenu de cet article est rédigé par Rust Core Team et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2013 Developpez.com.