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.
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");
}
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.