II. Spécificités de Rust
7. Déréférencement
Après les gros chapitres précédents, celui-là ne devrait pas vous prendre beaucoup de temps. Il vous arrivera de croiser ce genre de code :
Runfn une_fonction(x: &mut i32) {
*x = 2; // on déréférence
}
fn main() {
let mut x = 0;
println!("avant : {}", x);
une_fonction(&mut x);
println!("après : {}", x);
}
La valeur a donc été modifiée dans la fonction une_fonction
. Pour ceux ayant fait du C/C++, c'est exactement la même chose que le déréférencement d'un pointeur. La seule différence est que cela passe par les traits Deref et DerefMut en Rust.
Là où ça devient intéressant c'est que ces traits sont implémentés par "&" et "&mut". Donc "&" implémente Deref tandis que "&mut" implémente à la fois Deref et DerefMut. Ce qui permet de faire *x = 2
dans l'exemple précédent. Cependant, il est aussi possible de faire :
Runlet x = String::new();
// On déréférence &String en &str.
let deref_x: &str = &*x;
Il est donc possible de déréférencer un objet en implémentant ce trait.
Implémentation
On va prendre un exemple pour que vous compreniez le tout plus facilement :
Run// On importe le trait.
use std::ops::Deref;
struct UneStruct {
value: u32
}
impl Deref for UneStruct {
// Pour préciser quel type on retourne en déréférençant.
type Target = u32;
fn deref(&self) -> &u32 {
&self.value
}
}
fn main() {
let x = UneStruct { value: 0 };
assert_eq!(0u32, *x); // on peut maintenant déréférencer x
}
Je pense que le code est suffisamment explicite pour se passer d'explications supplémentaires.
Auto-déréférencement
Vous utilisez cette fonctionnalité sans le savoir lorsque vous faites :
Run// On a donc une String("toto").
let x = "toto".to_owned();
// On passe une &String comme argument à la fonction.
affiche_la_str(&x);
// On obtient une &str.
fn affiche_la_str(s: &str) {
println!("affichage : {}", s);
}
La question étant : "Pourquoi si on passe &String
on obtient &str
?". Sachez que Rust implémente un système d'auto-déréférencement (basé sur le trait Deref bien évidemment, rien de magique). Cela permet d'écrire des codes de ce genre :
Runstruct UneStruct;
impl UneStruct {
fn foo(&self) {
println!("UneStruct");
}
}
let f = UneStruct;
f.foo();
(&f).foo();
(&&f).foo();
(&&&&&&&&f).foo();
Le compilateur va déréférencer jusqu'à obtenir le type voulu (en l'occurrence, celui qui implémente la méthode foo
dans le cas présent, donc UneStruct
) ou jusqu'à renvoyer une erreur. Je pense que certains d'entre vous ont compris où je voulais en venir concernant String.
Le compilateur voit qu'on envoie &String
dans une méthode qui reçoit &str
comme paramètre. Il va donc déréférencer String pour obtenir &str
. Nous obtenons donc &str
. On peut imager ce que fait le compilateur de cette façon : &(*(String.deref()))
.
Pour ceux que ça intéresse, voici comment fait le compilateur, étape par étape :
&String
-> pas&str
, on déréférenceString
&(*String)
-> Le typeString
implémente le trait Deref, on appelle donc ce trait sur notre type.&(*(String.deref()))
&(*(&str))
&(str)
&str
Et voilà, le compilateur a bien le type attendu !