Aller au contenu

Les Références en Rust : des pointeurs

Comme on l’a vu précédemment avec les règles de l’ownership, lorsqu’on passe une variable en paramètre d’une fonction, un move a lieu et comme l’ownership est transféré à la fonction, la variable est alors invalide dans la suite du bloc.

Un petit exemple comme rappel :

fn calculate_length(s: String) -> usize {
s.len()
}
fn main() {
let s1 = String::from("hello");
let len = calculate_length(s1); // s1 est move vers le paramètre s de la fonction
println!("The length of '{}' is {}.", s1, len); // ERREUR car s1 a été move
}

Et ça, c’est la tête que tu risques d’en faire en ayant ce genre d’erreur :

Tu t’en doutes ce n’est pas très pratique d’avoir l’ownership qui est transféré systématiquement lorsqu’on passe une variable en paramètre d’une fonction…

Lorsqu’on ne veut pas transférer l’ownership d’une fonction, on va utiliser ce qu’on appelle des références qu’on note avec le symbole &.

C’est quoi une référence ?

Une référence (ou un pointeur dans d’autres langages) c’est une variable qui va rediriger vers une valeur en mémoire.

Une référence ne contient en mémoire que l’adresse mémoire de la valeur vers laquelle elle fait référence.

fn main(){
let a = String::from("Hello");
let b = &a;
}

Voici un bout de code où l’on crée une variable qui est une string “Hello” puis une variable b qui est une référence à a.

Sur le schéma, tu vois que a est une variable qui contient en mémoire la string “Hello”, okay rien de compliqué.

Puis tu vois qu’on a ensuite b qui est créé, cette fois on crée une référence vers la valeur en mémoire de a, ce qui se passe concrètement, c’est qu’on a en mémoire dans b, la valeur de l’adresse mémoire à laquelle est stockée a.

Ainsi quand on va utiliser b Rust va automatiquement aller chercher la valeur qui se trouve à l’adresse mémoire stockée dans b, c’est-à-dire la string “Hello”

“Mm Okay mais à quoi ça sert ?”

La principale utilité des références (ou des pointers dans d’autres langages) c’est pour des raisons de performances.

Avec ce système, on n’est pas obligé de copier toute la string “Hello” une deuxième fois en mémoire pour s’en servir dans une autre variable (ici b).

Dans ce cas de figure, ce ne serait pas très grave puisque la string n’est pas très grande, mais imagine faire la même chose avec un struct qui contient des dizaines de champs, cela couterait cher en termes de performances et de place à copier.

Raison pour laquelle les références sont très pratique.

Les règles du Borrowing

Le fait de passer une référence en Rust est appelé un emprunt, tout simplement parce que la fonction qui reçoit une référence va “emprunter” la variable qui se cache derrière la référence.

Comme pour les règles de l’ownership, il existe en Rust des règles concernant les références qu’on va nommer les règles du borrowing.

Ces règles nous disent qu’à tout le moment dans le programme on peut avoir :

  • Une ou plusieurs références immutables vers une ressource
  • Une seule référence mutable vers une ressource
  • MAIS pas les 2 en même temps

On va voir tout de suite ce que c’est dans la suite de l’article.

Les références immutables

Les références immutables sont des références dont on pourra uniquement lire la valeur, mais pas la modifier.

Jusqu’à présent quand on passait une string à une fonction, l’ownership était transféré et on ne pouvait plus utiliser la string dans la suite du code.

Grâce aux références on va maintenant pouvoir utiliser notre string librement après l’avoir passé en paramètres de la fonction, car on ne transferera pas l’ownership car celui ci sera simplement “emprunté” avec la référence.

fn show_name(name: &String){
println!("{}", name);
}
fn main(){
let john = String::from("John");
show_name(&john);
println!("{}",john);
}

Et voilà le travail ! Grâce aux références on peut maintenant utiliser john après son passage dans la fonction show_name.

Comme le dit la première règle du borrowing, il peut y avoir autant de référence immutable vers une ressource que l’on veut.

fn show_name(name: &String){
println!("{}", name);
}
fn main(){
let john = String::from("John");
// On a 3 refs immutables et tout fonctionne
let ref_one = &john;
let ref_two = &john;
let ref_three = &john;
show_name(&john);
println!("{}",john);
}

Par contre, on ne pourra pas modifier la valeur de john en passant une référence immutable, pour cela il faudra passer une référence mutable.

Les références mutables

Le but de la référence mutable est de pouvoir modifier la valeur vers laquelle pointe la référence.

Pour utiliser une référence mutable il faut absolument que la ressource derrière la référence soit mutable!

Et il faut ensuite utiliser la syntaxe &mut sur la référence.

fn double_string(x: &mut text){
*x = x.repeat(2);
}
fn main(){
// x doit être mutable si
// on veut l'utiliser via une ref mutable
let mut x = String::from("Hello");
double_string(&mut x);
}

“C’est quoi ce symbole ’*’?!!”

En Rust quand tu vois le symbole * avant une référence cela indique qu’on déréférence, c’est-à-dire qu’on récupère la valeur derrière la référence.

Ainsi quand on écrit *x = ... on indique qu’on modifie la valeur qui se cache derrière le pointeur et pas la référence elle-même.