Rust

Any donation is very welcome
Fork me on GitHub

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 le trait Deref en Rust. Dans l'exemple précédent, le trait est implémenté sur le type &mut i32. Cependant, il est aussi possible de faire :

Runlet x = String::new();
let deref_x = *x; // ce qui renvoie une erreur car le type str n'implémente pas le trait Sized

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 :

Runuse std::ops::Deref; // on importe le trait

struct UneStruct {
    value: u32
}

impl Deref for UneStruct {
    type Target = u32; // pour préciser quel type on retourne !

    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 :

Runfn affiche_la_str(s: &str) { // on obtient une &str
    println!("affichage : {}", s);
}

let x = "toto".to_owned(); // on a donc une String("toto")
affiche_la_str(&x); // on passe une &String

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. Ç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 !