Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Considérer les pointeurs intelligents comme des références grâce au trait Deref

L’implémentation du trait Deref vous permet de personnaliser le comportement de l’opérateur de déréférencement * (à ne pas confondre avec l’opérateur de multiplication ou le joker global). En implémentant Deref de manière à ce qu’un pointeur intelligent puisse être considéré comme une référence classique, vous pouvez écrire du code qui fonctionne avec des références mais aussi avec des pointeurs intelligents.

Regardons d’abord comment l’opérateur de déréférencement fonctionne avec des références classiques. Ensuite nous essaierons de définir un type personnalisé qui se comporte comme Box<T> et voir pourquoi l’opérateur de déréférencement ne fonctionne pas comme une référence sur notre type fraîchement défini. Nous allons découvrir comment implémenter le trait Deref de manière à ce qu’il soit possible que les pointeurs intelligents fonctionnent comme les références. Ensuite nous verrons la fonctionnalité d’extrapolation de déréférencement de Rust et comment elle nous permet de travailler à la fois avec des références et des pointeurs intelligents.

Suivre la référence à une valeur

Une référence classique est un type de pointeur, et une manière de modéliser un pointeur est d’imaginer une flèche pointant vers une valeur stockée autre part. Dans l’encart 15-6, nous créons une référence vers une valeur i32 et utilisons ensuite l’opérateur de déréférencement pour suivre la référence vers la valeur.

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-6: Using the dereference operator to follow a reference to an i32 value

La variable x stocke une valeur i32 : 5. Nous avons assigné à y une référence vers x. Nous pouvons faire une assert pour vérifier que x est égal à 5. Cependant, si nous souhaitons faire une assert sur la valeur dans y, nous devons utiliser *y pour suivre la référence vers la valeur sur laquelle elle pointe (d’où le déréférencement), de manière à ce que le compilateur puisse comparer la valeur réelle. Une fois que nous avons déréférencé y, nous avons accès à la valeur de l’entier sur laquelle y pointe afin que nous puissions la comparer avec 5.

Si nous avions essayé d’écrire assert_eq!(5, y); à la place, nous aurions obtenu cette erreur de compilation :

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src/main.rs:6:5
  |
6 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
  |
  = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
  = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)

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

Comparer un nombre et une référence vers un nombre n’est pas autorisé car ils sont de types différents. Nous devons utiliser l’opérateur de déréférencement pour suivre la référence vers la valeur sur laquelle elle pointe.

Utiliser Box<T> comme une référence

Nous pouvons réécrire le code l’encart 15-6 pour utiliser une Box<T> au lieu d’une référence ; l’opérateur de déréférencement utilisé sur le Box<T> dans l’encart 15-7 fonctionne de la même manière que l’opérateur de déréférencement utilisé sur la référence dans l’encart 15-6.

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-7: Using the dereference operator on a Box<i32>

La principale différence entre l’encart 15-7 et l’encart 15-6 est qu’ici nous avons fait en sorte que y soit une instance de boîte qui pointe sur une copie de la valeur de x plutôt que d’avoir une référence vers la valeur de x. Dans la dernière assertion, nous pouvons utiliser l’opérateur de déréférencement pour suivre le pointeur de la boîte de la même manière que nous l’avons fait lorsque y était une référence. Maintenant, nous allons regarder ce qu’il y a de si spécial dans Box<T> qui nous permet d’utiliser l’opérateur de déréférencement en définissant notre propre type de boîte.

Définir notre propre pointeur intelligent

Construisons un type conteneur (NdT : wrapper type) similaire au type Box<T> fourni par la bibliothèque standard pour apprendre comment les types de pointeurs intelligents se comportent différemment des références classiques. Nous regarderons ensuite comment lui ajouter la possibilité d’utiliser l’opérateur de déréférencement.

Remarque : il y a une grosse différence entre le type MaBoite<T> que nous allons construire et la vraie Box<T> : notre version ne va pas stocker ses données sur le tas. Nous allons concentrer cet exemple sur Deref, donc l’endroit où est concrètement stocké la donnée est moins important que le comportement similaire aux pointeurs.

Le type Box<T> est essentiellement défini comme étant une structure de tuple d’un seul élément, donc l’encart 15-8 définit un type MaBoite<T> de la même manière. Nous allons aussi définir une fonction new pour correspondre à la fonction new définie sur Box<T>.

Filename: src/main.rs
struct MaBoite<T>(T);

impl<T> MaBoite<T> {
    fn new(x: T) -> MaBoite<T> {
        MaBoite(x)
    }
}

fn main() {}
Listing 15-8: Defining a MyBox<T> type

Nous définissons une structure MaBoite et nous déclarons un paramètre générique T, car nous souhaitons que notre type stocke des valeurs de n’importe quel type. Le type MaBoite est une structure de tuple avec un seul élément de type T. La fonction MaBoite::new prend un paramètre de type T et retourne une instance MaBoite qui stocke la valeur qui lui est passée.

Essayons d’ajouter la fonction main de l’encart 15-7 dans l’encart 15-8 et de la modifier pour utiliser le type MaBoite<T> que nous avons défini à la place de Box<T>. Le code de l’encart 15-9 ne se compile pas car Rust ne sait pas comment déréférencer MaBoite.

Filename: src/main.rs
struct MaBoite<T>(T);

impl<T> MaBoite<T> {
    fn new(x: T) -> MaBoite<T> {
        MaBoite(x)
    }
}

fn main() {
    let x = 5;
    let y = MaBoite::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-9: Attempting to use MyBox<T> in the same way we used references and Box<T>

Voici l’erreur de compilation qui en résulte :

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MaBoite<{integer}>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^ can't be dereferenced

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

Notre type MaBoite<T> ne peut pas être déréférencée car nous n’avons pas implémenté cette fonctionnalité sur notre type. Pour permettre le déréférencement avec l’opérateur *, nous devons implémenter le trait Deref.

Implémentation du trait Deref

Comme nous l’avons vu dans “Implémenter un trait sur un type” au chapitre 10, pour implémenter un trait, nous devons fournir les implémentations des méthodes nécessaires pour ce trait. Le trait Deref, fourni par la bibliothèque standard, nécessite que nous implémentions une méthode deref qui emprunte self et retourne une référence vers la donnée interne. L’encart 15-10 contient une implémentation de Deref à ajouter à la définition de MaBoite<T> :

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

impl<T> Deref for MaBoite<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

struct MaBoite<T>(T);

impl<T> MaBoite<T> {
    fn new(x: T) -> MaBoite<T> {
        MaBoite(x)
    }
}

fn main() {
    let x = 5;
    let y = MaBoite::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-10: Implementing Deref on MyBox<T>

La syntaxe type Target = T; définit un type associé pour le trait Deref à utiliser. Les types associés sont une manière légèrement différente de déclarer un paramètre générique, mais vous n’avez pas à vous préoccuper d’eux pour le moment ; nous les verrons plus en détail au chapitre 20.

Nous renseignons le corps de la méthode deref avec &self.0 afin que deref retourne une référence vers la valeur que nous souhaitons accéder avec l’opérateur * ; rappellez-vous de la section “Création de différents types avec les structures tuples” du chapitre 5 où nous avons appris que le .0 accède à la première valeur d’une structure tuple. La fonction main de l’encart 15-9 qui appelle * sur la valeur MaBoite<T> se compile désormais, et le assert réussit aussi !

Sans le trait Deref, le compilateur peut seulement déréférencer des références &. La méthode deref donne la possibilité au compilateur d’obtenir la valeur de n’importe quel type qui implémente Deref en appelant la méthode deref pour obtenir une référence qu’il sait comment déréférencer.

Lorsque nous avons précisé *y dans l’encart 15-9, Rust fait tourner ce code en coulisses :

*(y.deref())

Rust remplace l’opérateur * par un appel à la méthode deref suivi par un simple déréférencement afin que nous n’ayons pas à nous demander si nous devons ou non appeler la méthode deref. Cette fonctionnalité de Rust nous permet d’écrire du code qui fonctionne de manière identique, que nous ayons une référence classique ou bien un type qui implémente Deref.

La raison pour laquelle la méthode deref retourne une référence à une valeur, et que le déréférencement du tout dans les parenthèses externes de *(y.deref()) reste nécessaire, a à voir avec le système de possession. Si la méthode deref retournait la valeur directement au lieu d’une référence à cette valeur, la valeur serait déplacée à l’extérieur de self. Nous ne souhaitons pas prendre possession de la valeur à l’intérieur de MaBoite<T> dans ce cas ainsi que dans la plupart des cas où nous utilisons l’opérateur de déréférencement.

Notez que l’opérateur * est remplacé par un appel à la méthode deref suivi par un appel à l’opérateur * une seule fois, à chaque fois que nous utilisons un * dans notre code. Comme la substitution de l’opérateur * ne s’effectue pas de manière récursive et infinie, nous récupèrerons une donnée de type i32, qui correspond au 5 du assert_eq! de l’encart 15-9.

Utilisation de l’extrapolation de déréférencement dans les fonctions et les méthodes

L’extrapolation de déréférencement convertit une référence vers un type qui implémente le trait Deref en une référence vers un autre type. Par exemple, l’extrapolation de déréférencement peut convertir &String en &str car String implémente le trait Deref de sorte qu’il puisse retourner &str. L’extrapolation de déréférencement est une commodité que Rust applique sur les arguments des fonctions et des méthodes, et elle fonctionne uniquement avec des types qui implémentent le trait Deref. Elle s’applique automatiquement lorsque nous passons une référence vers une valeur d’un type particulier en argument d’une fonction ou d’une méthode qui ne correspond pas à ce type de paramètre dans la définition de la fonction ou de la méthode. Une série d’appels à la méthode deref convertit le type que nous donnons dans le type que le paramètre nécessite.

L’extrapolation de déréférencement a été ajoutée à Rust afin de permettre aux développeurs d’écrire des appels de fonctions et de méthodes qui n’ont pas besoin d’indiquer explicitement les références et les déréférencements avec & et *. La fonctionnalité d’extrapolation de déréférencement nous permet aussi d’écrire plus de code qui peut fonctionner à la fois pour les références et pour les pointeurs intelligents.

Pour voir l’extrapolation de déréférencement en action, utilisons le type MaBoite<T> que nous avons défini dans l’encart 15-8 ainsi que l’implémentation de Deref que nous avons ajoutée dans l’encart 15-10. L’encart 15-11 montre la définition d’une fonction qui a un paramètre qui est une slice de chaîne de caractères.

Filename: src/main.rs
fn saluer(nom: &str) {
    println!("Salutations, {nom} !");
}

fn main() {}
Listing 15-11: A hello function that has the parameter name of type &str

Nous pouvons appeler la fonction saluer avec une slice de chaîne de caractères en argument, comme par exemple saluer("Rust");. L’extrapolation de déréférencement rend possible l’appel de saluer avec une référence à une valeur du type MaBoite<String>, comme dans l’encart 15-12.

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

impl<T> Deref for MaBoite<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MaBoite<T>(T);

impl<T> MaBoite<T> {
    fn new(x: T) -> MaBoite<T> {
        MaBoite(x)
    }
}

fn saluer(nom: &str) {
    println!("Salutations, {nom} !");
}

fn main() {
    let m = MaBoite::new(String::from("Rust"));
    saluer(&m);
}
Listing 15-12: Calling hello with a reference to a MyBox<String> value, which works because of deref coercion

Ici nous appelons la fonction saluer avec l’argument &m, qui est une référence vers une valeur de type MaBoite<String>. Comme nous avons implémenté le trait Deref sur MaBoite<T> dans l’encart 15-10, Rust peut transformer le &MaBoite<String> en &String en appelant deref. La bibliothèque standard fournit une implémentation de Deref sur String qui retourne une slice de chaîne de caractères, comme expliqué dans la documentation de l’API de Deref. Rust appelle à nouveau deref pour transformer le &String en &str, qui correspond à la définition de la fonction saluer.

Si Rust n’avait pas implémenté l’extrapolation de déréférencement, nous aurions dû écrire le code de l’encart 15-13 au lieu du code de l’encart 15-12 pour appeler saluer avec une valeur du type &MaBoite<String>.

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

impl<T> Deref for MaBoite<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MaBoite<T>(T);

impl<T> MaBoite<T> {
    fn new(x: T) -> MaBoite<T> {
        MaBoite(x)
    }
}

fn saluer(nom: &str) {
    println!("Salutations, {nom} !");
}

fn main() {
    let m = MaBoite::new(String::from("Rust"));
    saluer(&(*m)[..]);
}
Listing 15-13: The code we would have to write if Rust didn’t have deref coercion

Le (*m) déréférence la MaBoite<String> en une String. Ensuite le & et le [..] créent une slice de chaîne de caractères à partir de la String qui est égale à l’intégralité du contenu de la String, ceci afin de correspondre à la signature de saluer. Ce code sans l’extrapolation de déréférencement est bien plus difficile à lire, écrire et comprendre avec la présence de tous ces symboles. L’extrapolation de déréférencement permet à Rust d’automatiser ces convertions pour nous.

Lorsque le trait Deref est défini pour les types concernés, Rust va analyser les types et utiliser Deref::deref autant de fois que nécessaire pour obtenir une référence qui correspond au type du paramètre. Le nombre de fois qu’il est nécessaire d’insérer Deref::deref est résolu au moment de la compilation, ainsi il n’y a pas de surcoût au moment de l’exécution pour bénéficier de l’extrapolation de déréférencement !

Gestion de l’extrapolation de déréférencement avec des références mutables

De la même manière que vous pouvez utiliser le trait Deref pour remplacer le comportement de l’opérateur * sur les références immuables, vous pouvez utiliser le trait DerefMut pour remplacer le comportement de l’opérateur * sur les références mutables.

Rust procède à l’extrapolation de déréférencement lorsqu’il trouve des types et des implémentations de traits dans trois cas :

  1. Passer de &T à &U lorsque T: Deref<Target=U>
  2. Passer de &mut T à &mut U lorsque T: DerefMut<Target=U>
  3. Passer de &mut T à &U lorsque T: Deref<Target=U>

Les deux premiers cas sont exactement les mêmes, sauf que le second implémente la mutabilité. Le premier cas signifie que si vous avez un &T et que T implémente Deref pour le type U, vous pouvez obtenir un &U de manière transparente. Le deuxième cas signifie que la même extrapolation de déréférencement se déroule pour les références mutables.

Le troisième cas est plus ardu : Rust va aussi procéder à une extrapolation de déréférencement d’une référence mutable vers une référence immuable. Mais l’inverse n’est pas possible: une extrapolation de déréférencement d’une valeur immuable ne donnera jamais une référence mutable. À cause des règles d’emprunt, si vous avez une référence mutable, cette référence mutable doit être la seule référence vers cette donnée (autrement, le programme ne peut pas être compilé). Convertir une référence mutable vers une référence immuable ne va jamais casser les règles d’emprunt. Convertir une référence immuable vers une référence mutable nécessite que la référence immuable initiale soit la seule référence immuable vers cette donnée, mais les règles d’emprunt ne garantissent pas cela. Rust ne peut donc pas déduire que la conversion d’une référence immuable vers une référence mutable est possible.