Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Les références et l’emprunt

Le problème avec le code utilisant des tuples dans le listing 4-5 est que nous devons renvoyer la String à la fonction appelante afin de pouvoir encore utiliser la String après l’appel a calculate_length, car la String a été deplacee dans calculate_length. A la place, nous pouvons fournir une référence à la valeur String. Une référence est comme un pointeur en ce sens que c’est une adresse que nous pouvons suivre pour accéder aux données stockées à cette adresse ; ces données appartiennent à une autre variable. Contrairement à un pointeur, une référence est garantie de pointer vers une valeur valide d’un type particulier pendant toute la duree de vie de cette référence.

Voici comment vous definiriez et utiliseriez une fonction calculate_length qui prend une référence à un objet comme paramètre au lieu de prendre la possession de la valeur :

Filename: src/main.rs
fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

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

Premierement, remarquez que tout le code avec des tuples dans la déclaration de variable et la valeur de retour de la fonction a disparu. Deuxiemement, notez que nous passons &s1 a calculate_length et, dans sa définition, nous prenons &String plutot que String. Ces esperluettes représentent des références, et elles vous permettent de faire référence à une valeur sans en prendre la possession. La figure 4-6 illustre ce concept.

Trois tableaux : le tableau pour s contient uniquement un pointeur vers le tableau de s1. Le tableau de s1 contient les donnees de la pile pour s1 et pointe vers les donnees de chaine sur le tas. Figure 4-6 : Un diagramme de `&String` `s` pointant vers `String` `s1`

Note : L’oppose du referencement avec & est le dereferencement, qui s’effectue avec l’opérateur de dereferencement, *. Nous verrons quelques utilisations de l’opérateur de dereferencement au chapitre 8 et discuterons des détails du dereferencement au chapitre 15.

Examinons de plus près l’appel de fonction ici : rust {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-07-reference/src/main.rs:here}}

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

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

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

La syntaxe &s1 nous permet de créer une référence qui fait reference à la valeur de s1 mais ne la possède pas. Comme la référence ne la possède pas, la valeur vers laquelle elle pointe ne sera pas supprimee quand la référence cesse d’être utilisee.

De même, la signature de la fonction utilise & pour indiquer que le type du paramètre s est une référence. Ajoutons quelques annotations explicatives : rust {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-08-reference-with-annotations/src/main.rs:here}}

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

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize { // s is a référence to a String
    s.len()
} // Here, s goes out of scope. But because s does not have ownership of what
  // it refers to, the String is not dropped.

La portée dans laquelle la variable s est valide est la même que celle de tout paramètre de fonction, mais la valeur pointee par la référence n’est pas supprimee quand s cesse d’être utilisee, car s n’a pas la possession. Quand les fonctions prennent des références comme paramètres au lieu des valeurs réelles, nous n’aurons pas besoin de renvoyer les valeurs pour rendre la possession, car nous n’avons jamais eu la possession.

Nous appelons l’action de créer une référence l’emprunt (borrowing). Comme dans la vie réelle, si une personne possède quelque chose, vous pouvez le lui emprunter. Quand vous avez terminé, vous devez le rendre. Vous ne le possédez pas.

Alors, que se passe-t-il si nous essayons de modifier quelque chose que nous empruntons ? Essayez le code du listing 4-6. Attention spoiler : ca ne marche pas !

Filename: src/main.rs
fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}
Listing 4-6: Attempting to modify a borrowed value

Voici l’erreur : console {{#include ../listings/ch04-understanding-ownership/no-listing-14-dangling-reference/output.txt}}

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

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

Tout comme les variables sont immuables par défaut, les références le sont aussi. Nous ne sommes pas autorises a modifier quelque chose vers quoi nous avons une référence.

Les références mutables

Nous pouvons corriger le code du listing 4-6 pour nous permettre de modifier une valeur empruntée avec seulement quelques petites modifications qui utilisent, à la place, une reference mutable :

Filename: src/main.rs
fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

D’abord, nous changeons s pour qu’elle soit mut. Ensuite, nous créons une référence mutable avec &mut s la où nous appelons la fonction change et nous mettons à jour la signature de la fonction pour accepter une référence mutable avec some_string: &mut String. Cela rend très clair que la fonction change va modifier la valeur qu’elle emprunté.

Les références mutables ont une grande restriction : si vous avez une référence mutable vers une valeur, vous ne pouvez avoir aucune autre référence vers cette valeur. Ce code qui tente de créer deux références mutables vers s echouera :

Filename: 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 : console {{#include ../listings/ch04-understanding-ownership/no-listing-14-dangling-reference/output.txt}}

$ 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` (bin "ownership") due to 1 previous error

Cette erreur indique que ce code est invalide car nous ne pouvons pas emprunter s de maniere mutable plus d’une fois à la fois. Le premier emprunt mutable est dans r1 et doit durer jusqu’à son utilisation dans le println!, mais entre la creation de cette référence mutable et son utilisation, nous avons essaye de créer une autre référence mutable dans r2 qui emprunté les mêmes données que r1.

La restriction empechant plusieurs références mutables vers les mêmes données en même temps permet la mutation mais de maniere très contrôlée. C’est quelque chose avec lequel les nouveaux Rustaceans ont du mal car la plupart des langages vous permettent de modifier quand vous le souhaitez. L’avantage de cette restriction est que Rust peut prevenir les courses de données (data races) à la compilation. Une course de donnees est similaire à une condition de course (race condition) et se produit quand ces trois comportements surviennent :

  • Deux pointeurs ou plus accèdent aux mêmes données en même temps.
  • Au moins un des pointeurs est utilise pour écrire dans les données.
  • Aucun mecanisme n’est utilise pour synchroniser l’accès aux données.

Les courses de données causent un comportement indefini et peuvent être difficiles a diagnostiquer et corriger quand vous essayez de les traquer à l’exécution ; Rust empeche ce problème en refusant de compiler du code avec des courses de données !

Comme toujours, nous pouvons utiliser des accolades pour créer une nouvelle portée, ce qui permet d’avoir plusieurs références mutables, simplement pas simultanement : rust {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-11-muts-in-separate-scopes/src/main.rs:here}}

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

    {
        let r1 = &mut s;
    } // r1 goes out of scope here, so we can make a new référence with no problems.

    let r2 = &mut s;
}

Rust applique une règle similaire pour combiner des références mutables et immuables. Ce code produit une erreur : rust,ignore,does_not_compile {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-12-immutable-and-mutable-not-allowed/src/main.rs:here}}

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

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

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

Voici l’erreur : console {{#include ../listings/ch04-understanding-ownership/no-listing-14-dangling-reference/output.txt}}

$ 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; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{r1}, {r2}, and {r3}");
  |                -- immutable borrow later used here

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

Ouf ! Nous ne pouvons pas non plus avoir une référence mutable tant que nous avons une référence immuable vers la même valeur.

Les utilisateurs d’une référence immuable ne s’attendent pas à ce que la valeur change soudainement sous leurs pieds ! Cependant, plusieurs références immuables sont autorisees car personne qui se contente de lire les données n’à la capacité d’affecter la lecture des données par quelqu’un d’autre.

Notez que la portée d’une référence commence la où elle est introduite et continue jusqu’à la derniere utilisation de cette référence. Par exemple, ce code compilera car la derniere utilisation des références immuables se trouve dans le println!, avant que la référence mutable ne soit introduite : rust {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-13-reference-scope-ends/src/main.rs:here}}

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

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{r1} and {r2}");
    // Variables r1 and r2 will not be used after this point.

    let r3 = &mut s; // no problem
    println!("{r3}");
}

Les portées des références immuables r1 et r2 se terminent après le println! ou elles sont utilisees pour la derniere fois, ce qui est avant que la référence mutable r3 ne soit créée. Ces portées ne se chevauchent pas, donc ce code est autorise : le compilateur peut déterminer que la référence n’est plus utilisee à un moment avant la fin de la portée.

Même si les erreurs d’emprunt peuvent être frustrantes parfois, rappelez-vous que c’est le compilateur Rust qui signale un bug potentiel tot (à la compilation plutot qu’à l’exécution) et vous montre exactement ou se trouve le problème. Ainsi, vous n’avez pas a chercher pourquoi vos données ne sont pas ce que vous pensiez qu’elles étaient.

Les références pendantes (dangling références)

Dans les langages avec des pointeurs, il est facile de créer par erreur un pointeur pendant (dangling pointer) – un pointeur qui fait référence à un emplacement en mémoire qui a pu être donne a quelqu’un d’autre – en liberant de la mémoire tout en conservant un pointeur vers cette mémoire. En Rust, en revanche, le compilateur garantit que les références ne seront jamais des références pendantes : si vous avez une référence vers des données, le compilateur s’assurera que les données ne sortiront pas de la portée avant la référence vers ces données.

Essayons de créer une référence pendante pour voir comment Rust les empeche avec une erreur de compilation :

Filename: src/main.rs
fn main() {
    let reference_to_nothing = dangle();
}

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

    &s
}

Voici l’erreur : console {{#include ../listings/ch04-understanding-ownership/no-listing-14-dangling-reference/output.txt}}

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &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, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
5 | fn dangle() -> &'static String {
  |                 +++++++
help: instead, you are more likely to want to return an owned value
  |
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
  |

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

Ce message d’erreur fait référence à une fonctionnalité que nous n’avons pas encore couverte : les durees de vie (lifetimes). Nous discuterons des durees de vie en détail au chapitre 10. Mais, si vous ignorez les parties sur les durees de vie, le message contient la clé de la raison pour laquelle ce code pose problème :

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

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

Filename: src/main.rs
fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle returns a référence to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a référence to the String, s
} // Here, s goes out of scope and is dropped, so its memory goes away.
  // Danger!

Comme s est créée à l’intérieur de dangle, quand le code de dangle est terminé, s sera desallouee. Mais nous avons essaye de renvoyer une référence vers elle. Cela signifie que cette référence pointerait vers une String invalide. Ce n’est pas bon ! Rust ne nous laissera pas faire cela.

La solution ici est de renvoyer la String directement : rust {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-16-no-dangle/src/main.rs:here}}

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

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

    s
}

Cela fonctionne sans aucun problème. La possession est transferee, et rien n’est desalloue.

Les règles des références

Recapitulons ce que nous avons discute au sujet des références :

  • A tout moment, 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 valides.

Ensuite, nous examinerons un type différent de référence : les slices.