Le mécanisme Deref
Le problème résolu
Certaines conversions semblent fonctionner « sans raison apparente » :
&possedee est un &String, pas un &str. En toute logique, le code ne devrait pas compiler. C’est le trait Deref qui fait le travail en coulisses.
La définition
Deref est le trait qui indique « je sais me convertir en référence vers un autre type » :
Pour String, l’implémentation est, en simplifié :
Ce qui se lit : « à partir d’un &String, on peut obtenir un &str en appelant deref() ».
La coercition automatique
Le compilateur insère cet appel automatiquement lorsque le contexte l’exige. C’est ce que l’on appelle la deref coercion.
La coercition est transitive : si A: Deref<Target = B> et B: Deref<Target = C>, alors un &A peut être fourni là où un &C est attendu.
Info
La coercition n’a aucun coût à l’exécution : elle est résolue à la compilation et ne produit ni copie ni déplacement. On reste dans le domaine des références, donc de l’emprunt — voir Ownership et emprunt.
Les déréférencements courants
| Type source | Cible (Target) | Coercition obtenue |
|---|---|---|
String | str | &String → &str |
Vec<T> | [T] | &Vec<i32> → &[i32] |
Box<T> | T | &Box<Config> → &Config |
Rc<T> | T | &Rc<Config> → &Config |
Arc<T> | T | &Arc<Config> → &Config |
PathBuf | Path | &PathBuf → &Path |
OsString | OsStr | &OsString → &OsStr |
CString | CStr | &CString → &CStr |
C’est ce qui permet d’écrire une seule fonction prenant un &str et de l’appeler indifféremment avec un &String, un &Box<str> ou un littéral, sans écrire trois variantes.
Tip
D’où la règle de conception : typer les paramètres avec le type emprunté plutôt qu’avec le type possédé.
À l’inverse, fn compter(mots: &Vec<String>) force l’appelant à posséder un Vec et interdit de passer une portion.
L’opérateur *
Le déréférencement peut aussi être écrit manuellement avec * :
En pratique on ne l’écrit presque jamais, la coercition automatique s’en chargeant. Il reste utile pour lever une ambiguïté ou forcer une conversion que le compilateur ne peut pas déduire.
L’appel de méthodes
Deref sert également à la résolution de méthodes : les méthodes du type cible deviennent appelables sur le type source.
Face à s.len(), le compilateur cherche la méthode successivement sur String, puis &String, puis — via Deref — sur str et &str, jusqu’à trouver une correspondance. C’est ce que l’on appelle le deref chaining.
Note
Une grande partie de la « richesse » de String provient en réalité de str, simplement exposée par Deref. Le même raisonnement vaut pour Vec<T>, dont beaucoup de méthodes (iter(), first(), windows(), sort()…) appartiennent en fait à la slice [T].
Un cas concret : les wrappers
Le mécanisme explique aussi pourquoi les types enveloppes s’utilisent de façon transparente. Avec le framework web Axum, un extracteur d’état s’écrit ainsi :
State<T> implémente Deref<Target = T>. L’accès à state.db est donc réécrit en (*state).db, soit le champ db de l’AppState contenu. Il n’est pas nécessaire de déstructurer le wrapper au préalable.
Faut-il implémenter Deref soi-même ?
Presque jamais, sauf pour écrire un pointeur intelligent. La convention de la communauté est la suivante : n’implémenter Deref que si le type est une variante d’un autre type (relation « est-un »), pas s’il en contient un (relation « a-un »).
Warning
Utiliser Deref pour de la simple délégation est déroutant pour qui lit le code : les méthodes exposées ne sont plus celles du type, et rien ne le signale à l’appel. Pour partager des comportements, les traits AsRef<T> et Borrow<T> sont préférables — ils réalisent la même conversion, mais de façon explicite.
DerefMut
Le trait jumeau DerefMut fait de même pour les références mutables, avec la contrainte d’avoir d’abord implémenté Deref :
Il permet la coercition &mut String → &mut str, ou l’appel des méthodes mutantes du type cible :
À retenir
Derefpermet à Rust de traiter un&Enveloppe<T>comme un&Tlorsque le contexte le demande.
C’est une coercition silencieuse, résolue à la compilation, et non une conversion coûteuse : on reste sur une référence, donc sans copie ni déplacement. C’est ce qui explique qu’autant de choses fonctionnent « comme par magie » : &String accepté en &str, &Vec<T> accepté en &[T], accès direct aux champs à travers un Box, un Arc ou un extracteur.
Ressources
std::ops::Deref— documentation du trait- Deref coercion — chapitre du Rust Book consacré aux pointeurs intelligents
AsRefetBorrow— les alternatives explicites