Introduction
Bienvenue dans Treating Smart Pointers Like Regular References With Deref. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.
Dans ce laboratoire, nous allons explorer comment l'implémentation du trait Deref permet de traiter les pointeurs intelligents comme des références normales et comment la fonction de coercition de déréférence de Rust permet de travailler avec des références ou des pointeurs intelligents.
Treating Smart Pointers Like Regular References with Deref
L'implémentation du trait Deref vous permet de personnaliser le comportement de l'opérateur de déréférence * (ne pas le confondre avec l'opérateur de multiplication ou de glisser-déposer). En implémentant Deref de manière à ce qu'un pointeur intelligent puisse être traité comme une référence normale, vous pouvez écrire du code qui opère sur des références et utiliser ce code avec des pointeurs intelligents également.
Regardons d'abord comment l'opérateur de déréférence fonctionne avec les références normales. Ensuite, nous allons essayer de définir un type personnalisé qui se comporte comme Box<T>, et voir pourquoi l'opérateur de déréférence ne fonctionne pas comme une référence sur notre nouveau type 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. Ensuite, nous examinerons la fonction de coercition de déréférence de Rust et la manière dont elle nous permet de travailler avec des références ou des pointeurs intelligents.
Note : Il y a 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 où les données sont effectivement stockées est moins important que le comportement ressemblant à un pointeur.
Following the Pointer to the Value
Une référence normale est un type de pointeur, et une manière de concevoir un pointeur est comme une flèche vers une valeur stockée ailleurs. Dans la Liste 15-6, nous créons une référence à une valeur i32 puis utilisons l'opérateur de déréférence pour suivre la référence jusqu'à la valeur.
Nom du fichier : src/main.rs
fn main() {
1 let x = 5;
2 let y = &x;
3 assert_eq!(5, x);
4 assert_eq!(5, *y);
}
Liste 15-6 : Utilisation de l'opérateur de déréférence pour suivre une référence vers une valeur i32
La variable x contient une valeur i32 égale à 5 [1]. Nous définissons y égal à une référence à x [2]. Nous pouvons affirmer que x est égal à 5 [3]. Cependant, si nous voulons faire une assertion sur la valeur de y, nous devons utiliser *y pour suivre la référence jusqu'à la valeur à laquelle elle pointe (d'où le terme déréférencement) afin que le compilateur puisse comparer la valeur réelle [4]. Une fois que nous avons déréférencé y, nous avons accès à la valeur entière à 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 :
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}`
Comparer un nombre et une référence à un nombre n'est pas autorisé car ce sont des types différents. Nous devons utiliser l'opérateur de déréférence pour suivre la référence jusqu'à la valeur à laquelle elle pointe.
Using Box<T> Like a Reference
Nous pouvons réécrire le code de la Liste 15-6 pour utiliser un Box<T> au lieu d'une référence ; l'opérateur de déréférence utilisé sur le Box<T> dans la Liste 15-7 fonctionne de la même manière que l'opérateur de déréférence utilisé sur la référence dans la Liste 15-6.
Nom du fichier : src/main.rs
fn main() {
let x = 5;
1 let y = Box::new(x);
assert_eq!(5, x);
2 assert_eq!(5, *y);
}
Liste 15-7 : Utilisation de l'opérateur de déréférence sur un Box<i32>
La principale différence entre la Liste 15-7 et la Liste 15-6 est que ici, nous définissons y comme une instance d'un box pointant vers une valeur copiée de x plutôt qu'une référence pointant vers la valeur de x [1]. Dans la dernière assertion [2], nous pouvons utiliser l'opérateur de déréférence pour suivre le pointeur du box de la même manière que lorsque y était une référence. Ensuite, nous explorerons ce qui est spécial à propos de Box<T> qui nous permet d'utiliser l'opérateur de déréférence en définissant notre propre type de box.
Defining Our Own Smart Pointer
Construisons un pointeur intelligent similaire au type Box<T> fourni par la bibliothèque standard pour découvrir comment les pointeurs intelligents se comportent différemment des références par défaut. Ensuite, nous examinerons comment ajouter la capacité d'utiliser l'opérateur de déréférence.
Le type Box<T> est finalement défini comme une struct tuple avec un élément, donc la Liste 15-8 définit un type MyBox<T> de la même manière. Nous définirons également une fonction new pour correspondre à la fonction new définie sur Box<T>.
Nom du fichier : src/main.rs
1 struct MyBox<T>(T);
impl<T> MyBox<T> {
2 fn new(x: T) -> MyBox<T> {
3 MyBox(x)
}
}
Liste 15-8 : Définition d'un type MyBox<T>
Nous définissons une struct nommée MyBox et déclarons un paramètre générique T [1] car nous voulons que notre type puisse stocker des valeurs de tout 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 [2] et renvoie une instance de MyBox qui contient la valeur passée en paramètre [3].
Essayons d'ajouter la fonction main de la Liste 15-7 à la Liste 15-8 et de la modifier pour utiliser le type MyBox<T> que nous avons défini au lieu de Box<T>. Le code de la Liste 15-9 ne compilera pas car Rust ne sait pas comment déréférencer MyBox.
Nom du fichier : src/main.rs
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Liste 15-9 : Tentative d'utilisation de MyBox<T> de la même manière que les références et Box<T>
Voici l'erreur de compilation résultante :
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
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 autoriser le déréférencement avec l'opérateur *, nous implémentons le trait Deref.
Implementing the Deref Trait
Comme discuté dans "Implementing a Trait on a Type", 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 oblige à implémenter une méthode nommée deref qui emprunte self et renvoie une référence à la donnée interne. La Liste 15-10 contient une implémentation de Deref à ajouter à la définition de MyBox``<T>.
Nom du fichier : src/main.rs
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
1 type Target = T;
fn deref(&self) -> &Self::Target {
2 &self.0
}
}
Liste 15-10 : Implémentation de Deref sur MyBox<T>
La syntaxe type Target = T; [1] 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 besoin de vous en soucier pour l'instant ; nous en aborderons les détails plus en détail au Chapitre 19.
Nous remplissons le corps de la méthode deref avec &self.0 de sorte que deref renvoie une référence à la valeur que nous voulons accéder avec l'opérateur * [2] ; rappelez-vous de "Using Tuple Structs Without Named Fields to Create Different Types" que .0 accède à la première valeur dans une struct tuple. La fonction main de la Liste 15-9 qui appelle * sur la valeur MyBox<T> compile désormais, et les assertions sont validées!
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 tout type qui implémente Deref et d'appeler la méthode deref pour obtenir une référence & qu'il sait déréférencer.
Lorsque nous avons entré *y dans la Liste 15-9, en coulisse, Rust a effectivement exécuté ce code :
*(y.deref())
Rust remplace l'opérateur * par un appel à la méthode deref puis un simple déréférencement, de sorte que nous n'ayons pas besoin de 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 normale ou un type qui implémente Deref.
La raison pour laquelle la méthode deref renvoie une référence à 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 propriété. Si la méthode deref renvoyait directement la valeur au lieu d'une référence à la valeur, la valeur serait déplacée hors de self. Nous ne voulons pas prendre la propriété de la valeur interne dans MyBox<T> dans ce cas ou dans la plupart des cas où nous utilisons l'opérateur de déréférence.
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. Étant donné que la substitution de l'opérateur * ne se poursuit pas indéfiniment, nous obtenons finalement des données de type i32, qui correspondent à 5 dans assert_eq! de la Liste 15-9.
Implicit Deref Coercions with Functions and Methods
La deref coercion convertit une référence à un type qui implémente le trait Deref en une référence à un autre type. Par exemple, la deref coercion peut convertir &String en &str car String implémente le trait Deref de manière à renvoyer &str. La deref coercion est une commodité que Rust applique aux arguments de fonctions et de méthodes, et ne fonctionne que sur les types qui implémentent le trait Deref. Elle se produit automatiquement lorsque nous passons une référence à une valeur d'un type particulier en tant qu'argument à une fonction ou à une méthode qui ne correspond pas au type de paramètre 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 requis par le paramètre.
La deref coercion a été ajoutée à Rust afin 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érences explicites avec & et *. La fonction deref coercion nous permet également d'écrire plus de code qui peut fonctionner avec des références ou des pointeurs intelligents.
Pour voir la deref coercion en action, utilisons le type MyBox<T> que nous avons défini dans la Liste 15-8 ainsi que l'implémentation de Deref que nous avons ajoutée dans la Liste 15-10. La Liste 15-11 montre la définition d'une fonction qui a un paramètre de type slice de chaîne de caractères.
Nom du fichier : src/main.rs
fn hello(name: &str) {
println!("Hello, {name}!");
}
Liste 15-11 : Une fonction hello qui a le paramètre name de type &str
Nous pouvons appeler la fonction hello avec une slice de chaîne de caractères en tant qu'argument, par exemple hello("Rust");. La deref coercion permet d'appeler hello avec une référence à une valeur de type MyBox<String>, comme montré dans la Liste 15-12.
Nom du fichier : src/main.rs
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
Liste 15-12 : Appel de hello avec une référence à une valeur MyBox<String>, qui fonctionne grâce à la deref coercion
Ici, nous appelons la fonction hello avec l'argument &m, qui est une référence à une valeur MyBox<String>. En raison de l'implémentation du trait Deref sur MyBox<T> dans la Liste 15-10, Rust peut convertir &MyBox<String> en &String en appelant deref. La bibliothèque standard fournit une implémentation de Deref sur String qui renvoie une slice de chaîne de caractères, et cela est dans la documentation API de Deref. Rust appelle deref à nouveau pour convertir le &String en &str, qui correspond à la définition de la fonction hello.
Si Rust n'avait pas implémenté la deref coercion, nous aurions dû écrire le code de la Liste 15-13 au lieu du code de la Liste 15-12 pour appeler hello avec une valeur de type &MyBox<String>.
Nom du fichier : src/main.rs
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
Liste 15-13 : Le code que nous aurions dû écrire si Rust n'avait pas la deref coercion
Le (*m) déréférence le MyBox<String> en une String. Ensuite, le & et [..] prennent une slice de chaîne de caractères de la String qui est égale à la chaîne entière pour correspondre à la signature de hello. Ce code sans deref coercion est plus difficile à lire, à écrire et à comprendre avec tous ces symboles en jeu. La deref coercion permet à Rust de gérer automatiquement ces conversions pour nous.
Lorsque le trait Deref est défini pour les types impliqués, Rust analysera les types et utilisera Deref::deref autant de fois que nécessaire pour obtenir une référence qui corresponde au type du paramètre. Le nombre de fois où Deref::deref doit être inséré est résolu à la compilation, donc il n'y a pas de pénalité exécution pour profiter de la deref coercion!
How Deref Coercion Interacts with Mutability
De la même manière que vous utilisez le trait Deref pour surcharger l'opérateur * sur les références immuables, vous pouvez utiliser le trait DerefMut pour surcharger l'opérateur * sur les références mutables.
Rust effectue une deref coercion lorsqu'il trouve des types et des implémentations de traits dans trois cas :
- De
&Tà&UlorsqueT: Deref<Target=U> - De
&mut Tà&mut UlorsqueT: DerefMut<Target=U> - De
&mut Tà&UlorsqueT: Deref<Target=U>
Les deux premiers cas sont identiques, sauf que le second implémente la mutabilité. Le premier cas stipule que si vous avez une &T, et que T implémente Deref vers un certain type U, vous pouvez obtenir une &U de manière transparente. Le second cas stipule que la même deref coercion se produit pour les références mutables.
Le troisième cas est plus compliqué : Rust coercera également une référence mutable en une référence immutable. Mais l'inverse n'est pas possible : les références immuables ne seront jamais coercées 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 à cette donnée (sinon, le programme ne compilerait pas). Convertir une référence mutable en une référence immutable ne brisera jamais les règles d'emprunt. Convertir une référence immutable en une référence mutable nécessiterait que la référence immutable initiale soit la seule référence immutable à cette donnée, mais les règles d'emprunt ne garantissent pas cela. Par conséquent, Rust ne peut pas supposer que la conversion d'une référence immutable en une référence mutable est possible.
Summary
Félicitations! Vous avez terminé le laboratoire Traiter les pointeurs intelligents comme des références normales avec Deref. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.