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 *
(qui n'est pas 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 essayerons 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.
Remarque : il y a une grosse différence entre le type
MaBoite<T>
que nous allons construire et la vraieBox<T>
: notre version ne va pas stocker ses données sur le tas. Nous allons concentrer cet exemple surDeref
, donc l'endroit où est concrètement stocké la donnée est moins important que le comportement similaire aux pointeurs.
Suivre le pointeur vers la valeur grâce à l'opérateur de déréférencement
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 donnée :
Fichier : src/main.rs
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
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). 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` due to 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 étant 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 devrait fonctionner comme
montré dans l'encart 15-7 :
Fichier : src/main.rs
fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
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 boite qui pointe sur une copie
de la valeur de x
plutôt qu'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 boite 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 boite.
Définir notre propre pointeur intelligent
Construisons un pointeur intelligent similaire au type Box<T>
fourni par la
bibliothèque standard pour apprendre comment les pointeurs intelligents se
comportent différemment des références classiques. Ensuite nous regarderons
comment lui ajouter la possibilité d'utiliser l'opérateur de déréférencement.
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>
.
Fichier : src/main.rs
struct MaBoite<T>(T); impl<T> MaBoite<T> { fn new(x: T) -> MaBoite<T> { MaBoite(x) } } fn main() {}
Nous définissons une structure MaBoite
et on déclare 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 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
.
Fichier : 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);
}
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);
| ^^
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` due to 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
.
Considérer un type comme une référence en implémentant le trait Deref
Comme nous l'avons vu dans une section du 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
:
Fichier : 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); }
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 19.
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 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 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, est 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 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.
Extrapolation de déréférencement implicite avec les fonctions et les méthodes
L'extrapolation de déréférencement est une commodité que Rust applique sur les
arguments des fonctions et des méthodes. L'extrapolation de déréférencement
fonctionne uniquement avec un type qui implémente le trait Deref
.
L'extrapolation de déréférencement convertit une référence vers ce type 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
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 :
Fichier : src/main.rs
fn saluer(nom: &str) { println!("Salutations, {} !", nom); } fn main() {}
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 :
Fichier : 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); }
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>
.
Fichier : 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)[..]); }
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
. Le 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 !
L'interaction de l'extrapolation de déréférencement avec la mutabilité
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 :
- Passer de
&T
à&U
lorsqueT: Deref<Target=U>
- Passer de
&mut T
à&mut U
lorsqueT: DerefMut<Target=U>
- Passer de
&mut T
à&U
lorsqueT: Deref<Target=U>
Les deux premiers cas sont exactement les mêmes, sauf pour 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. A 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.