Les références et l’emprunt
Le problème avec le code utilisant des tuples dans l’encart 4-5 est que nous devons renvoyer la String à la fonction appelante afin de pouvoir encore utiliser la String après l’appel à calculate_length, car la String a été déplacée dans calculate_length. À 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 durée de vie de cette référence.
Voici comment vous définiriez 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 :
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()
}
Premièrement, remarquez que tout le code avec des tuples dans la déclaration de variable et la valeur de retour de la fonction a disparu. Deuxièmement, notez que nous passons &s1 à calculate_length et, dans sa définition, nous prenons &String plutôt 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.
Note : L’opposé du référencement avec
&est le déréférencement, qui s’effectue avec l’opérateur de déréférencement,*. Nous verrons quelques utilisations de l’opérateur de déréférencement au chapitre 8 et discuterons des détails du déréférencement au chapitre 15.
Examinons de plus près l’appel de fonction ici :
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 référence à 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 supprimée quand la référence cesse d’être utilisée.
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 :
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 reference 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 pointée par la référence n’est pas supprimée quand s cesse d’être utilisée, 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 de l’encart 4-6. Attention spoiler : ca ne marche pas !
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
Voici l’erreur :
$ 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 it 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 autorisés à modifier quelque chose vers quoi nous avons une référence.
Les références mutables
Nous pouvons corriger le code de l’encart 4-6 pour nous permettre de modifier une valeur empruntée avec seulement quelques petites modifications qui utilisent, à la place, une référence mutable :
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 là 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 emprunte.
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 :
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` (bin "ownership") due to 1 previous error
Cette erreur indique que ce code est invalide car nous ne pouvons pas emprunter s de manière 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 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 les mêmes données que r1.
La restriction empêchant plusieurs références mutables vers les mêmes données en même temps permet la mutation mais de manière 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 prévenir les courses de données (data races) à la compilation. Une course de données 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 indéfini et peuvent être difficiles à diagnostiquer et corriger quand vous essayez de les traquer à l’exécution ; Rust empêche 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 :
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference 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 :
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 :
$ 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 :
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! où elles sont utilisées pour la dernière 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 autorisé : le compilateur peut déterminer que la référence n’est plus utilisée à 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 tôt (à la compilation plutôt qu’à l’exécution) et vous montre exactement où se trouve le problème. Ainsi, vous n’avez pas à 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 :
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &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 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 durées de vie (lifetimes). Nous discuterons des durées de vie en détail au chapitre 10. Mais, si vous ignorez les parties sur les durées 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 :
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference 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 :
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
Récapitulons ce que nous avons discuté 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.