Traiter les pointeurs intelligents comme des références classiques
Implémenter le 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 glob). En implémentant Deref de manière à ce qu’un pointeur intelligent puisse être traité comme une référence classique, vous pouvez écrire du code qui opère sur des références et utiliser ce code aussi avec des pointeurs intelligents.
Commençons par voir 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 nous verrons pourquoi l’opérateur de déréférencement ne fonctionne pas comme une référence sur notre type nouvellement défini. Nous explorerons comment l’implémentation du trait Deref permet aux pointeurs intelligents de fonctionner de manière similaire aux références. Puis, nous examinerons la fonctionnalité de coercition de déréférencement de Rust et comment elle nous permet de travailler avec des références ou des pointeurs intelligents.
Suivre la référence jusqu’à la valeur
Une référence classique est un type de pointeur, et une façon de penser à un pointeur est comme une flèche vers une valeur stockée ailleurs. Dans l’encart 15-6, nous créons une référence vers une valeur i32 puis utilisons l’opérateur de déréférencement pour suivre la référence jusqu’à la valeur.
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
i32 valueLa variable x contient une valeur i32 de 5. Nous définissons y égal à une référence vers x. Nous pouvons vérifier que x est égal à 5. Cependant, si nous voulons faire une assertion sur la valeur dans y, nous devons utiliser *y pour suivre la référence jusqu’à la valeur vers laquelle elle pointe (d’où le déréférencement) afin que le compilateur puisse comparer la valeur réelle. Une fois que nous déréférençons y, nous avons accès à la valeur entière vers laquelle y pointe, que nous pouvons comparer avec 5.
Si nous avions essayé d’écrire assert_eq!(5, y); à la place, nous aurions obtenu cette erreur de compilation : console {{#include ../listings/ch15-smart-pointers/output-only-01-comparing-to-reference/output.txt}}
$ 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 ce sont des types différents. Nous devons utiliser l’opérateur de déréférencement pour suivre la référence jusqu’à la valeur vers laquelle elle pointe.
Utiliser Box<T> comme une référence
Nous pouvons réécrire le code de l’encart 15-6 pour utiliser un 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 de l’encart 15-6.
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Box<i32>La principale différence entre l’encart 15-7 et l’encart 15-6 est qu’ici nous définissons y comme une instance d’une boîte pointant vers une copie de la valeur de x plutôt qu’une référence pointant 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 lorsque y était une référence. Ensuite, nous explorerons ce qui est spécial dans Box<T> et 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 enveloppeur similaire au type Box<T> fourni par la bibliothèque standard pour expérimenter comment les types de pointeurs intelligents se comportent différemment des références par défaut. Ensuite, nous verrons comment ajouter la possibilité d’utiliser l’opérateur de déréférencement.
Remarque : il y à une grande différence entre le type
MyBox<T>que nous allons construire et le vraiBox<T>: notre version ne stockera pas ses données sur le tas. Nous concentrons cet exemple surDeref, donc l’endroit où les données sont réellement stockées est moins important que le comportement de type pointeur.
Le type Box<T> est fondamentalement défini comme une struct tuple avec un seul élément, donc l’encart 15-8 définit un type MyBox<T> de la même manière. Nous définirons également une fonction new correspondant à la fonction new définie sur Box<T>.
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {}
MyBox<T> typeNous définissons une struct nommée MyBox et déclarons un paramètre générique T car nous voulons que notre type puisse contenir des valeurs de n’importe quel type. Le type MyBox est une struct tuple avec un élément de type T. La fonction MyBox::new prend un paramètre de type T et retourné une instance MyBox qui contient la valeur passée.
Essayons d’ajouter la fonction main de l’encart 15-7 à l’encart 15-8 en la modifiant pour utiliser le type MyBox<T> que nous avons défini au lieu de Box<T>. Le code de l’encart 15-9 ne compilera pas, car Rust ne sait pas comment déréférencer MyBox.
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
MyBox<T> in the same way we used references and Box<T>Voici l’erreur de compilation résultante : console {{#include ../listings/ch15-smart-pointers/listing-15-09/output.txt}}
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{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 MyBox<T> ne peut pas être déréférencé car nous n’avons pas implémenté cette capacité sur notre type. Pour activer le déréférencement avec l’opérateur *, nous implémentons le trait Deref.
Implémenter le trait Deref
Comme abordé dans « Implémenter un trait sur un type » au chapitre 10, pour implémenter un trait nous devons fournir des implémentations pour les méthodes requises du trait. Le trait Deref, fourni par la bibliothèque standard, nous demande d’implémenter une méthode nommée deref qui emprunté self et retourné une référence vers les données internes. L’encart 15-10 contient une implémentation de Deref à ajouter à la définition de MyBox<T>.
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Deref on MyBox<T>La syntaxe type Target = T; définit un type associé que le trait Deref utilisera. 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 besoin de vous en préoccuper pour l’instant ; nous les couvrirons plus en détail au chapitre 20.
Nous remplissons le corps de la méthode deref avec &self.0 pour que deref retourné une référence vers la valeur que nous voulons accéder avec l’opérateur * ; rappelez-vous de [“Créer des types différents avec les structs tuples”][tuple-structs] au chapitre 5 que .0 accède à la première valeur d’une struct tuple. La fonction main de l’encart 15-9 qui appelle * sur la valeur MyBox<T> compilé maintenant, et les assertions passent !
Sans le trait Deref, le compilateur ne peut déréférencer que les références &. La méthode deref donne au compilateur la capacité de prendre une valeur de n’importe quel type qui implémente Deref et d’appeler la méthode deref pour obtenir une référence qu’il sait comment déréférencer.
Quand nous avons saisi *y dans l’encart 15-9, derrière les coulisses, Rust a en fait exécuté ce code :
*(y.deref())
Rust substitue l’opérateur * par un appel à la méthode deref suivi d’un déréférencement simple afin que nous n’ayons pas à nous demander si nous devons appeler la méthode deref ou non. 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 retourné une référence vers une valeur, et que le déréférencement simple en dehors des parenthèses dans *(y.deref()) est toujours nécessaire, est liée au système de possession. Si la méthode deref retournait la valeur directement au lieu d’une référence vers la valeur, la valeur serait déplacée hors de self. Nous ne voulons pas prendre la possession de la valeur intérieure dans MyBox<T> dans ce cas ni 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 puis 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 récurse pas indéfiniment, nous obtenons des données de type i32, qui correspondent au 5 dans assert_eq! de l’encart 15-9.
Utiliser la coercition de déréférencement dans les fonctions et les méthodes
La coercition de déréférencement (deref coercion) 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, la coercition de déréférencement peut convertir &String en &str car String implémente le trait Deref de manière à retourner &str. La coercition de déréférencement est une facilité que Rust effectue sur les arguments des fonctions et des méthodes, et elle ne fonctionne que sur les types qui implémentent le trait Deref. Elle se produit automatiquement lorsque nous passons une référence vers la valeur d’un type particulier comme argument à une fonction ou une méthode dont le type de paramètre ne correspond pas dans la définition de la fonction ou de la méthode. Une séquence d’appels à la méthode deref convertit le type que nous avons fourni en le type dont le paramètre a besoin.
La coercition de déréférencement a été ajoutée à Rust pour que les programmeurs écrivant des appels de fonctions et de méthodes n’aient pas besoin d’ajouter autant de références et de déréférencements explicites avec & et *. La fonctionnalité de coercition de déréférencement nous permet également d’écrire plus de code qui fonctionne aussi bien avec des références qu’avec des pointeurs intelligents.
Pour voir la coercition de déréférencement en action, utilisons le type MyBox<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 à un paramètre de type slice de chaîne de caractères.
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {}
hello function that has the parameter name of type &strNous pouvons appeler la fonction hello avec une slice de chaîne de caractères comme argument, comme hello("Rust"); par exemple. La coercition de déréférencement rend possible l’appel de hello avec une référence vers une valeur de type MyBox<String>, comme montré dans l’encart 15-12.
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
hello with a reference to a MyBox<String> value, which works because of deref coercionIci, nous appelons la fonction hello avec l’argument &m, qui est une référence vers une valeur MyBox<String>. Comme nous avons implémenté le trait Deref sur MyBox<T> dans l’encart 15-10, Rust peut transformer &MyBox<String> en &String en appelant deref. La bibliothèque standard fournit une implémentation de Deref sur String qui retourné une slice de chaîne de caractères, et cela se trouve dans la documentation de l’API de Deref. Rust appelle deref à nouveau pour transformer le &String en &str, ce qui correspond à la définition de la fonction hello.
Si Rust n’implémentait pas la coercition de déréférencement, nous devrions écrire le code de l’encart 15-13 au lieu du code de l’encart 15-12 pour appeler hello avec une valeur de type &MyBox<String>.
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
Le (*m) déréférence le MyBox<String> en un String. Ensuite, le & et le [..] prennent une slice de chaîne de caractères du String qui est égale à la chaîne entière pour correspondre à la signature de hello. Ce code sans coercition de déréférencement est plus difficile à lire, écrire et comprendre avec tous ces symboles impliqués. La coercition de déréférencement permet à Rust de gérer ces conversions pour nous automatiquement.
Quand le trait Deref est défini pour les types concernés, Rust analysera les types et utilisera Deref::deref autant de fois que nécessaire pour obtenir une référence correspondant au type du paramètre. Le nombre de fois où Deref::deref doit être inséré est résolu à la compilation, il n’y a donc aucune pénalité à l’exécution pour tirer parti de la coercition de déréférencement !
Gérer la coercition de déréférencement avec les références mutables
De la même manière que vous utilisez le trait Deref pour redéfinir l’opérateur * sur les références immuables, vous pouvez utiliser le trait DerefMut pour redéfinir l’opérateur * sur les références mutables.
Rust effectue la coercition de déréférencement lorsqu’il trouve des types et des implémentations de traits dans trois cas :
- De
&Tvers&UquandT: Deref<Target=U> - De
&mut Tvers&mut UquandT: DerefMut<Target=U> - De
&mut Tvers&UquandT: Deref<Target=U>
Les deux premiers cas sont identiques sauf que le deuxième implémente la mutabilité. Le premier cas indique que si vous avez un &T, et que T implémente Deref vers un certain type U, vous pouvez obtenir un &U de manière transparente. Le deuxième cas indique que la même coercition de déréférencement se produit pour les références mutables.
Le troisième cas est plus délicat : Rust convertira aussi une référence mutable en une référence immuable. Mais l’inverse n’est pas possible : les références immuables ne seront jamais converties en références mutables. En raison 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 ces données (sinon, le programme ne compilerait pas). Convertir une référence mutable en une référence immuable ne violera jamais les règles d’emprunt. Convertir une référence immuable en une référence mutable nécessiterait que la référence immuable initiale soit la seule référence immuable vers ces données, mais les règles d’emprunt ne le garantissent pas. Par conséquent, Rust ne peut pas supposer que convertir une référence immuable en une référence mutable est possible.