Thanks!
X
Paypal
Github sponsorship
Patreon

Rust

Fork me on GitHub

II. Spécificités de Rust

6. 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 sont "&" 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 aussi ce trait sans le savoir quand 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);
}

Normalement, vous devriez vous demander : "Pourquoi si on passe &String on obtient &str ?!". Hé bien sachez que Rust implémente un système d'auto-déréférencement (basé sur le trait Deref bien évidemment, rien de magique). Ça 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érence String
  • &(*String) -> Le type String implémente le trait Deref, on l'appelle
  • &(*(String.deref()))
  • &(*(&str))
  • &(str)
  • &str

Et voilà, le compilateur a bien le type attendu !