Le mécanisme Deref

Le problème résolu

Certaines conversions semblent fonctionner « sans raison apparente » :

fn afficher(s: &str) {
    println!("{s}");
}

let possedee = String::from("Hello");
afficher(&possedee);        // Compile, alors que &possedee est de type &String

&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 » :

pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
    //       ^^^^^         ^^^^^^^^
    //      emprunt      référence vers le type ciblé
}

Pour String, l’implémentation est, en simplifié :

impl Deref for String {
    type Target = str;
    fn deref(&self) -> &str { /* renvoie le contenu de la String */ }
}

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.

afficher(&possedee);
// Le compilateur raisonne ainsi :
//   - afficher attend un &str
//   - on lui passe un &String
//   - or String: Deref<Target = str>
//   - donc il insère l'appel à deref()

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 sourceCible (Target)Coercition obtenue
Stringstr&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
PathBufPath&PathBuf → &Path
OsStringOsStr&OsString → &OsStr
CStringCStr&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é.

fn compter(mots: &[String]) -> usize { mots.len() }    // Accepte &Vec<String> et &[String]
fn afficher(nom: &str) {}                              // Accepte &String, &str, &Box<str>

À 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 * :

let s: String = String::from("Hello");
let r: &str = &*s;          // *s appelle deref(), &*s produit un &str

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.

let s: String = String::from("Hello");

s.len();                    // len() n'est pas défini sur String...
s.to_uppercase();           // ...ni to_uppercase() : ce sont des méthodes de str,
s.contains("ell");          //    atteintes via Deref

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 :

async fn handler(state: State<AppState>) {
    let db = &state.db;     // AppState n'est pourtant pas directement accessible
}

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 »).

use std::ops::Deref;

struct Metres(f64);

impl Deref for Metres {
    type Target = f64;
    fn deref(&self) -> &f64 { &self.0 }
}

let distance = Metres(42.0);
println!("{}", distance.sqrt());        // Méthode de f64, atteinte via Deref
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.

fn afficher<S: AsRef<str>>(valeur: S) {
    println!("{}", valeur.as_ref());
}

afficher("littéral");
afficher(String::from("possédée"));

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 :

pub trait DerefMut: Deref {
    fn deref_mut(&mut self) -> &mut Self::Target;
}

Il permet la coercition &mut String → &mut str, ou l’appel des méthodes mutantes du type cible :

let mut v = vec![3, 1, 2];
v.sort();                   // sort() est une méthode de [T], pas de Vec<T>

À retenir

Deref permet à Rust de traiter un &Enveloppe<T> comme un &T lorsque 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