Les références et l'emprunt

La difficulté avec le code du tuple à la fin de la section précédente est que nous avons besoin de retourner la String au code appelant pour qu'il puisse continuer à utiliser la String après l'appel à calculer_taille, car la String a été déplacée dans calculer_taille. À la place, nous pouvons fournir une référence à la valeur de la String. Une référence est comme un pointeur dans le sens où c'est une adresse que nous pouvons suivre pour accéder à la donnée stockée à cette adresse qui est possédée par une autre variable. Mais contrairement aux pointeurs, une référence garantit de pointer vers une valeur en vigueur, d'un type bien déterminé. Voici comment définir et utiliser une fonction calculer_taille qui prend une référence à un objet en paramètre plutôt que de prendre possession de la valeur :

Fichier : src/main.rs

fn main() {
    let s1 = String::from("hello");

    let long = calculer_taille(&s1);

    println!("La taille de '{}' est {}.", s1, long);
}

fn calculer_taille(s: &String) -> usize {
    s.len()
}

Premièrement, on peut observer que tout le code des tuples dans la déclaration des variables et dans la valeur de retour de la fonction a été enlevé. Deuxièmement, remarquez que nous passons &s1 à calculer_taille, et que dans sa définition, nous utilisons &String plutôt que String. Ces esperluettes représentent les références, et elles permettent de vous référer à une valeur sans en prendre possession. L'illustration 4-5 illustre ce concept.

&String s qui pointe vers la String s1

Illustration 4-5 : Un schéma de la &String s qui pointe vers la String s1

Remarque : l'opposé de la création de références avec & est le déréférencement, qui s'effectue avec l'opérateur de déréférencement, *. Nous allons voir quelques utilisations de l'opérateur de déréférencement dans le chapitre 8 et nous aborderons les détails du déréférencement dans le chapitre 15.

Regardons de plus près l'appel à la fonction :

fn main() {
    let s1 = String::from("hello");

    let long = calculer_taille(&s1);

    println!("La taille de '{}' est {}.", s1, long);
}

fn calculer_taille(s: &String) -> usize {
    s.len()
}

La syntaxe &s1 nous permet de créer une référence qui se réfère à la valeur de s1 mais n'en prend pas possession. Et comme elle ne la possède pas, la valeur vers laquelle elle pointe ne sera pas libérée quand cette référence ne sera plus utilisée.

De la même manière, la signature de la fonction utilise & pour indiquer que le type du paramètre s est une référence. Ajoutons quelques commentaires explicatifs :

fn main() {
    let s1 = String::from("hello");

    let long = calculer_taille(&s1);

    println!("La taille de '{}' est {}.", s1, long);
}

fn calculer_taille(s: &String) -> usize { // s est une référence à une String
  s.len()
} // Ici, s sort de la portée. Mais comme elle ne prend pas possession de ce
  // à quoi elle fait référence, il ne se passe rien.

La portée dans laquelle la variable s est en vigueur est la même que toute portée d'un paramètre de fonction, mais la valeur pointée par la référence n'est pas libérée quand s n'est plus utilisé, car s n'en prends pas possession. Lorsque les fonctions ont des références en paramètres au lieu des valeurs réelles, nous n'avons pas besoin de retourner les valeurs pour les rendre, car nous n'en avons jamais pris possession.

Nous appelons l'emprunt l'action de créer une référence. Comme dans la vie réelle, quand un objet appartient à quelqu'un, vous pouvez le lui emprunter. Et quand vous avez fini, vous devez le lui rendre. Vous ne le possédez pas.

Donc qu'est-ce qui se passe si nous essayons de modifier quelque chose que nous empruntons ? Essayez le code dans l'encart 4-6. Attention, spoiler : cela ne fonctionne pas !

Fichier : src/main.rs

fn main() {
    let s = String::from("hello");

    changer(&s);
}

fn changer(texte: &String) {
    texte.push_str(", world");
}

Entrée 4-6 : Tentative de modification d'une valeur empruntée.

Voici l'erreur :

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*texte` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
7 | fn changer(texte: &String) {
  |                   ------- help: consider changing this to be a mutable reference: `&mut String`
8 |     texte.push_str(", world");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^ `texte` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` due to previous error

Comme les variables sont immuables par défaut, les références le sont aussi. Nous ne sommes pas autorisés à modifier une chose quand nous avons une référence vers elle.

Les références mutables

Nous pouvons résoudre le code de l'encart 4-6 pour nous permettre de modifier une valeur empruntée avec quelques petites modification qui utilisent plutôt une référence mutable :

Fichier : src/main.rs

fn main() {
    let mut s = String::from("hello");

    changer(&mut s);
}

fn changer(texte: &mut String) {
    texte.push_str(", world");
}

D'abord, nous précisons que s est mut. Ensuite, nous avons créé une référence mutable avec &mut s où nous appelons la fonction change et nous avons modifié la signature pour accepter de prendre une référence mutable avec texte: &mut String. Cela précise clairement que la fonction change va faire muter la valeur qu'elle emprunte.

Les références mutables ont une grosse contrainte : vous ne pouvez avoir qu'une seule référence mutable pour chaque donnée au même moment. Le code suivant qui va tenter de créer deux références mutables à s va échouer :

Fichier : src/main.rs

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);
}

Voici l'erreur :

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 | 
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` due to previous error

Cette erreur nous explique que ce code est invalide car nous ne pouvons pas emprunter s de manière mutable plus d'une fois au même moment. Le premier emprunt mutable est dans r1 et doit perdurer jusqu'à ce qu'il soit utilisé dans le println!, mais pourtant entre la création de cette référence mutable et son utilisation, nous avons essayé de créer une autre référence mutable dans r2 qui emprunte la même donnée que dans r1.

La limitation qui empêche d'avoir plusieurs références mutables vers la même donnée au même moment autorise les mutations, mais de manière très contrôlée. C'est quelque chose que les nouveaux Rustacés ont du mal à surmonter, car la plupart des langages vous permettent de modifier les données quand vous le voulez. L'avantage d'avoir cette contrainte est que Rust peut empêcher les accès concurrents au moment de la compilation. Un accès concurrent est une situation de concurrence qui se produit lorsque ces trois facteurs se combinent :

  • Deux pointeurs ou plus accèdent à la même donnée au même moment.
  • Au moins un des pointeurs est utilisé pour écrire dans cette donnée.
  • On n'utilise aucun mécanisme pour synchroniser l'accès aux données.

L'accès concurrent provoque des comportements indéfinis et rend difficile le diagnostic et la résolution de problèmes lorsque vous essayez de les reproduire au moment de l'exécution ; Rust évite ce problème en refusant de compiler du code avec des accès concurrents !

Comme d'habitude, nous pouvons utiliser des accolades pour créer une nouvelle portée, pour nous permettre d'avoir plusieurs références mutables, mais pas en même temps :

fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    } // r1 sort de la portée ici, donc nous pouvons créer une nouvelle référence
      // sans problèmes.

    let r2 = &mut s;
}

Rust impose une règle similaire pour combiner les références immuables et mutables. Ce code va mener à une erreur :

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // sans problème
    let r2 = &s; // sans problème
    let r3 = &mut s; // GROS PROBLEME
    
    println!("{}, {}, and {}", r1, r2, r3);
}

Voici l'erreur :

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // sans problème
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // sans problème
6 |     let r3 = &mut s; // GROS PROBLEME
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error

Ouah ! Nous ne pouvons pas non plus avoir une référence mutable pendant que nous en avons une autre immuable vers la même valeur. Les utilisateurs d'une référence immuable ne s'attendent pas à ce que sa valeur change soudainement ! Cependant, l'utilisation de plusieurs références immuables ne pose pas de problème, car simplement lire une donnée ne va pas affecter la lecture de la donnée par les autres.

Notez bien que la portée d'une référence commence dès qu'elle est introduite et se poursuit jusqu'au dernier endroit où cette référence est utilisée. Par exemple, le code suivant va se compiler car la dernière utilisation de la référence immuable, le println!, est située avant l'introduction de la référence mutable :

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // sans problème
    let r2 = &s; // sans problème
    println!("{} et {}", r1, r2);
    //les variables r1 et r2 ne seront plus utilisés à partir d'ici
    
    let r3 = &mut s; // sans problème
    println!("{}", r3);
}

Les portées des références immuables r1 et r2 se terminent après le println! où elles sont utilisées pour la dernière fois, c'est-à-dire avant que la référence mutable r3 soit créée. Ces portées ne se chevauchent pas, donc ce code est autorisé. La capacité du compilateur à dire si une référence n'est plus utilisée à un endroit avant la fin de la portée s'appelle en Anglais les Non-Lexical Lifetimes (ou NLL), et vous pouvez en apprendre plus dans le Guide de l'édition.

Même si ces erreurs d'emprunt peuvent parfois être frustrantes, n'oubliez pas que le compilateur de Rust nous signale un bogue potentiel en avance (au moment de la compilation plutôt que l'exécution) et vous montre où se situe exactement le problème. Ainsi, vous n'avez pas à chercher pourquoi vos données ne correspondent pas à ce que vous pensiez qu'elles devraient être.

Les références pendouillantes

Avec les langages qui utilisent les pointeurs, il est facile de créer par erreur un pointeur pendouillant (dangling pointer), qui est un pointeur qui pointe vers un emplacement mémoire qui a été donné à quelqu'un d'autre, en libérant de la mémoire tout en conservant un pointeur vers cette mémoire. En revanche, avec Rust, le compilateur garantit que les références ne seront jamais des références pendouillantes : si nous avons une référence vers une donnée, le compilateur va s'assurer que cette donnée ne va pas sortir de la portée avant que la référence vers cette donnée en soit elle-même sortie.

Essayons de créer une référence pendouillante pour voir comment Rust va les empêcher via une erreur au moment de la compilation :

Fichier : src/main.rs

fn main() {
    let reference_vers_rien = pendouille();
}

fn pendouille() -> &String {
    let s = String::from("hello");

    &s
}

Voici l'erreur :

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn pendouille() -> &String {
  |                    ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn pendouille() -> &'static String {
  |                    ~~~~~~~~

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` due to previous error

Ce message d'erreur fait référence à une fonctionnalité que nous n'avons pas encore vue : les durées de vie. Nous aborderons les durées de vie dans le chapitre 10. Mais, si vous mettez de côté les parties qui parlent de durées de vie, le message explique pourquoi le code pose problème :

this function's return type contains a borrowed value, but there is no value
for it to be borrowed from

Ce qui peut se traduire par :

Le type de retour de cette fonction contient une valeur empruntée, mais il n'y a
plus aucune valeur qui peut être empruntée.

Regardons de plus près ce qui se passe exactement à chaque étape de notre code de pendouille :

Fichier : src/main.rs

fn main() {
    let reference_vers_rien = pendouille();
}

fn pendouille() -> &String { // pendouille retourne une référence vers une String

  let s = String::from("hello"); // s est une nouvelle String

  &s // nous retournons une référence vers la String, s
} // Ici, s sort de la portée, et est libéré. Sa mémoire disparaît.
  // Attention, danger !

Comme s est créé dans pendouille, lorsque le code de pendouille est terminé, la variable s sera désallouée. Mais nous avons essayé de retourner une référence vers elle. Cela veut dire que cette référence va pointer vers une String invalide. Ce n'est pas bon ! Rust ne nous laissera pas faire cela.

Ici la solution est de renvoyer la String directement :

fn main() {
    let string = ne_pendouille_pas();
}

fn ne_pendouille_pas() -> String {
    let s = String::from("hello");

    s
}

Cela fonctionne sans problème. La possession est transférée à la valeur de retour de la fonction, et rien n'est désalloué.

Les règles de référencement

Récapitulons ce que nous avons vu à propos des références :

  • À un instant donné, vous pouvez avoir soit une référence mutable, soit un nombre quelconque de références immuables.
  • Les références doivent toujours être en vigueur.

Ensuite, nous aborderons un autre type de référence : les slices.