La conformité des références avec les durées de vies
Il reste un détail que nous n'avons pas abordé dans la section “Les références et l'emprunt” du chapitre 4, c'est que toutes les références ont une durée de vie dans Rust, qui est la portée pour laquelle cette référence est en vigueur. La plupart du temps, les durées de vies sont implicites et sont déduites automatiquement, comme pour la plupart du temps les types sont déduits. Nous devons renseigner le type lorsque plusieurs types sont possibles. De la même manière, nous devons renseigner les durées de vie lorsque les durées de vies des références peuvent être déduites de différentes manières. Rust nécessite que nous renseignons ces relations en utilisant des paramètres de durée de vie génériques pour s'assurer que les références utilisées au moment de la compilation restent bien en vigueur.
L'annotation de la durée de vie n'est pas un concept présent dans la pluspart des langages de programmation, donc cela n'est pas très familier. Bien que nous ne puissions couvrir l'intégralité de la durée de vie dans ce chapitre, nous allons voir les cas les plus courants où vous allez rencontrer la syntaxe de la durée de vie, pour vous introduire ces concept.
Eviter les références pendouillantes avec les durées de vie
L'objectif principal des durées de vies est d'éviter les références pendouillantes qui font qu'un programme pointe des données autres que celles sur lesquelles il était censé pointer. Soit le programme de l'encart 10-17, qui a une portée externe et une portée interne.
fn main() {
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
}
Remarque : Les exemples dans les encarts 10-17, 10-18 et 10-24 déclarent des variables sans initialiser leur valeur, donc les noms de ces variables existent dans la portée externe. A première vue, cela semble être en conflit avec le fonctionnement de Rust qui n'utilise pas les valeurs nulles. Cependant, si nous essayons d'utiliser une variable avant de lui donner une valeur, nous aurons une erreur au moment de la compilation, qui confirme que Rust ne fonctionne pas avec des valeurs nulles.
La portée externe déclare une variable r
sans valeur initiale, et la portée
interne déclare une variable x
avec la valeur initiale à 5
. Au sein de la
portée interne, nous essayons d'assigner la valeur de r
comme étant une
référence à x
. Puis la portée interne se ferme, et nous essayons d'afficher la
valeur dans r
. Ce code ne va pas se compiler car la valeur r
se réfère à
quelque chose qui est sorti de la portée avant que nous essayons de l'utiliser.
Voici le message d'erreur :
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
--> src/main.rs:7:17
|
7 | r = &x;
| ^^ borrowed value does not live long enough
8 | }
| - `x` dropped here while still borrowed
9 |
10 | println!("r: {}", r);
| - borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error
La variable x
n'existe plus (“does not live long enough”). La raison à cela
est que x
est sortie de la portée lorsque la portée interne s'est fermée à la
ligne 7. Mais r
reste en vigueur dans la portée externe ; car sa portée est
plus grande, on dit qu'il “vit plus longtemps”. Si Rust avait permis à ce code de
s'exécuter, r
pointerait sur de la mémoire désallouée dès que x
est sortie
de la portée, ainsi tout ce que nous pourrions faire avec r
ne fonctionnerait
pas correctement. Mais comment Rust détecte que ce code est invalide ? Il
utilise le vérificateur d'emprunt.
Le vérificateur d'emprunt
Le compilateur de Rust embarque un vérificateur d'emprunt (borrow checker) qui compare les portées pour déterminer si les emprunts sont valides. L'encart 10-18 montre le même code que l'encart 10-17, mais avec des commentaires qui montrent les durées de vies des variables.
fn main() {
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
}
Ici, nous avons montré la durée de vie de r
avec 'a
et la durée de vie de
x
avec 'b
. Comme vous pouvez le constater, le bloc interne 'b
est bien
plus petit que le bloc externe 'a
. Au moment de la compilation, Rust compare
les tailles des deux durées de vies et constate que r
a la durée de vie 'a
mais fait référence à de la mémoire qui a une durée de vie de 'b
. Ce programme
est refusé car 'b
est plus court que 'a
: l'élément pointé par la référence
n'existe pas aussi longtemps que la référence.
L'encart 10-19 résout le code afin qu'il n'ait plus de référence pendouillante et qu'il se compile sans erreur.
fn main() { { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!("r: {}", r); // | | // --+ | } // ----------+ }
Ici, x
a la durée de vie 'b
, qui est plus grande dans ce cas que 'a
. Cela
signifie que r
peut référencer x
car Rust sait que la référence présente
dans r
sera toujours valide du moment que x
est en vigueur.
Maintenant que vous savez où se situent les durées de vie des références et comment Rust analyse les durées de vies pour s'assurer que les références soient toujours en vigueur, découvrons les durées de vies génériques des paramètres et des valeurs de retour dans le cas des fonctions.
Les durées de vies génériques dans les fonctions
Ecrivons une fonction qui retourne la plus longue des slice d'une chaîne de
caractères. Cette fonction va prendre en argument deux slices de chaîne de
caractères et retourner une slice d'une chaîne de caractères. Après avoir
implémenté la fonction la_plus_longue
, le code de l'encart 10-20 devrait
afficher La plus grande chaîne est abcd
.
Fichier : src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let resultat = la_plus_longue(string1.as_str(), string2);
println!("La plus grande chaîne est {}", resultat);
}
Remarquez que nous souhaitons que la fonction prenne deux slices de chaînes de
caractères, qui sont des références, car nous ne voulons pas que la fonction
la_plus_longue
prenne possession de ses paramètres. Rendez-vous à la section
“Les slices de chaînes de caractères en
paramètres” du chapitre 4 pour
savoir pourquoi nous utilisons ce type de paramètres dans l'encart 10-20.
Si nous essayons d'implémenter la fonction la_plus_longue
comme dans l'encart
10-21, cela ne va pas se compiler.
Fichier : src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let resultat = la_plus_longue(string1.as_str(), string2);
println!("La plus grande chaîne est {}", resultat);
}
fn la_plus_longue(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
A la place, nous obtenons l'erreur suivante qui nous parle de durées de vie :
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn la_plus_longue(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn la_plus_longue<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` due to previous error
La partie “help” nous explique que le type de retour a besoin d'un paramètre de
durée de vie générique car Rust ne sait pas si la référence retournée est liée à
x
ou à y
. Pour le moment, nous ne le savons pas nous non plus, car le bloc
if
dans le corps de cette fonction retourne une référence à x
et le bloc
else
retourne une référence à y
!
Lorsque nous définissons cette fonction, nous ne connaissons pas les valeurs
concrètes qui vont passer dans cette fonction, donc nous ne savons pas si nous
allons exécuter le cas du if
ou du else
. Nous ne connaissons pas non plus les
durées de vie des références qui vont passer dans la fonction, donc nous ne
pouvons pas vérifier les portées comme nous l'avons fait dans les encarts 10-18
et 10-19 pour déterminer si la référence que nous allons retourner sera
toujours en vigueur. Le vérificateur d'emprunt ne va pas pouvoir non plus
déterminer cela, car il ne sait comment les durées de vie de x
et de y
sont
reliées à la durée de vie de la valeur de retour. Pour résoudre cette erreur,
nous allons ajouter des paramètres de durée de vie génériques qui définissent
la relation entre les références, afin que le vérificateur d'emprunt puisse
faire cette analyse.
La syntaxe pour annoter les durées de vies
L'annotation des durées de vie ne change pas la longueur de leur durée de vie. De la même façon qu'une fonction accepte n'importe quel type lorsque la signature utilise un paramètre de type générique, les fonctions peuvent accepter des références avec n'importe quelle durée de vie en précisant un paramètre de durée de vie générique. L'annotation des durées de vie décrit la relation des durées de vies de plusieurs références entre elles sans influencer les durées de vie.
L'annotation des durées de vies a une syntaxe un peu inhabituelle : le nom des
paramètres de durées de vies doit commencer par une apostrophe ('
) et est
habituellement en minuscule et très court, comme les types génériques. La
plupart des personnes utilisent le nom 'a
. Nous plaçons le paramètre de type
après le &
d'une référence, en utilisant un espace pour séparer l'annotation
du type de la référence.
Voici quelques exemples : une référence à un i32
sans paramètre de durée de
vie, une référence à un i32
qui a un paramètre de durée de vie 'a
, et une
référence mutable à un i32
qui a aussi la durée de vie 'a
.
&i32 // une référence
&'a i32 // une référence avec une durée de vie explicite
&'a mut i32 // une référence mutable avec une durée de vie explicite
Une annotation de durée de vie toute seule n'a pas vraiment de sens, car les
annotations sont faites pour indiquer à Rust quels paramètres de durée de vie
génériques de plusieurs références sont liés aux autres. Par exemple, disons que
nous avons une fonction avec le paramètre premier
qui est une référence à un
i32
avec la durée de vie 'a
. La fonction a aussi un autre paramètre second
qui est une autre référence à un i32
qui a aussi la durée de vie 'a
. Les
annotations de durée de vie indiquent que les références premier
et second
doivent tous les deux exister aussi longtemps que la durée de vie générique.
Les annotations de durée de vie dans les signatures des fonctions
Maintenant, examinons les annotations de durée de vie dans contexte de la
fonction la_plus_longue
. Comme avec les paramètres de type génériques, nous
devons déclarer les paramètres de durée de vie génériques dans des chevrons
entre le nom de la fonction et la liste des paramètres. Nous souhaitons
contraindre les durées de vie des deux paramètres et la durée de vie de la
référence retournée de telle manière que la valeur retournée restera en vigueur
tant que les deux paramètres le seront aussi. Nous allons appeler la durée de
vie 'a
et ensuite l'ajouter à chaque référence, comme nous le faisons dans
l'encart 10-22.
Fichier : src/main.rs
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let resultat = la_plus_longue(string1.as_str(), string2); println!("La plus grande chaîne est {}", resultat); } fn la_plus_longue<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
Le code devrait se compiler et devrait produire le résultat que nous souhaitions
lorsque nous l'utilisions dans la fonction main
de l'encart 10-20.
La signature de la fonction indique maintenant à Rust que pour la durée de vie
'a
, la fonction prend deux paramètres, les deux étant des slices de chaîne de
caractères qui vivent aussi longtemps que la durée de vie 'a
. La signature de
la fonction indique également à Rust que la slice de chaîne de caractères qui est
retournée par la fonction vivra au moins aussi longtemps que la durée de vie
'a
. Dans la pratique, cela veut dire que durée de vie de la référence
retournée par la fonction la_plus_longue
est la même que celle de la plus
petite des durées de vies des références qu'on lui donne. Cette relation est ce
que nous voulons que Rust mette en place lorsqu'il analysera ce code.
Souvenez-vous, lorsque nous précisons les paramètres de durée de vie dans la
signature de cette fonction, nous ne changeons pas les durées de vies des
valeurs qui lui sont envoyées ou qu'elle retourne. Ce que nous faisons, c'est
plutôt indiquer au vérificateur d'emprunt qu'il doit rejeter toute valeur qui
ne répond pas à ces conditions. Notez que la fonction la_plus_longue
n'a pas
besoin de savoir exactement combien de temps x
et y
vont exister, mais
seulement que cette portée peut être substituée par 'a
, qui satisfera cette
signature.
Lorsqu'on précise les durées de vie dans les fonctions, les annotations se placent dans la signature de la fonction, pas dans le corps de la fonction. Les annotations de durée de vie sont devenues partie intégrante de la fonction, exactement comme les types dans la signature. Avoir des signatures de fonction qui intègrent la durée de vie signifie que l'analyse que va faire le compilateur Rust sera plus simple. S'il y a un problème avec la façon dont la fonction est annotée ou appelée, les erreurs de compilation peuvent pointer plus précisément sur la partie de notre code qui impose ces contraintes. Mais si au contraire, le compilateur Rust avait dû faire plus de suppositions sur ce que nous voulions créer comme lien de durée de vie, le compilateur n'aurait pu qu'évoquer une utilisation de notre code bien plus éloignée de la véritable raison du problème.
Lorsque nous donnons une référence concrète à la_plus_longue
, la durée de vie
concrète qui est modélisée par 'a
est la partie de la portée de x
qui se
chevauche avec la portée de y
. Autrement dit, la durée vie générique 'a
aura
la durée de vie concrète qui est égale à la plus petite des durées de vies entre
x
et y
. Comme nous avons marqué la référence retournée avec le même
paramètre de durée de vie 'a
, la référence retournée sera toujours en vigueur
pour la durée de la plus petite des durées de vies de x
et de y
.
Regardons comment les annotations de durée de vie restreignent la fonction
la_plus_longue
en y passant des références qui ont des durées de vies
concrètement différentes. L'encart 10-23 en est un exemple.
Fichier : src/main.rs
fn main() { let string1 = String::from("une longue chaîne est longue"); { let string2 = String::from("xyz"); let resultat = la_plus_longue(string1.as_str(), string2.as_str()); println!("La chaîne la plus longue est {}", resultat); } } fn la_plus_longue<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
Dans cet exemple, string1
est en vigueur jusqu'à la fin de la portée externe,
string2
n'est valide que jusqu'à la fin de la portée interne, et resultat
est une référence vers quelque chose qui est en vigueur jusqu'à la fin de la
portée interne. Lorsque vous lancez ce code, vous constaterez que le
vérificateur d'emprunt accepte ce code ; il va se compiler et afficher
La chaîne la plus longue est une longue chaîne est longue
.
Maintenant, essayons un exemple qui fait en sorte que la durée de vie de la
référence dans resultat
sera plus petite que celles des deux arguments. Nous
allons déplacer la déclaration de la variable resultat
à l'extérieur de la
portée interne mais on va laisser l'affectation de la valeur de la variable
resultat
à l'intérieur de la portée de string2
. Nous allons ensuite déplacer
le println!
, qui utilise resultat
, à l'extérieur de la portée interne, après
que la portée soit terminée. Le code de l'encart 10-24 ne va pas se compiler.
Fichier : src/main.rs
fn main() {
let string1 = String::from("une longue chaîne est longue");
let resultat;
{
let string2 = String::from("xyz");
resultat = la_plus_longue(string1.as_str(), string2.as_str());
}
println!("La chaîne la plus longue est {}", resultat);
}
fn la_plus_longue<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Lorsque nous essayons de compiler ce code, nous aurons cette erreur :
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
6 | result = la_plus_longue(string1.as_str(), string2.as_str());
| ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("La chaîne la plus longue est {}", resultat);
| -------- borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error
L'erreur explique que pour que resultat
soit en vigueur pour l'instruction
println!
, string2
doit toujours être valide jusqu'à la fin de la portée
externe. Rust a déduit cela car nous avons précisé les durées de vie des
paramètres de la fonction et des valeurs de retour en utilisant le même
paramètre de durée de vie 'a
.
En tant qu'humain, nous pouvons lire ce code et constater que string1
est plus
grand que string2
et ainsi que resultat
contiendra une référence vers
string1
. Comme string1
n'est pas encore sorti de portée, une référence vers
string1
sera toujours valide pour l'instruction println!
. Cependant, le
compilateur ne peut pas déduire que la référence est valide dans notre cas. Nous
avons dit à Rust que la durée de vie de la référence qui est retournée par la
fonction la_plus_longue
est la même que la plus petite des durées de vie des
références qu'on lui passe en argument. C'est pourquoi le vérificateur d'emprunt
rejette le code de l'encart 10-24 car il a potentiellement une référence
invalide.
Essayez d'expérimenter d'autres situations en variant les valeurs et durées de
vie des références passées en argument de la fonction la_plus_longue
, et
aussi pour voir comment on utilise la référence retournée. Faites des
hypothèses pour savoir si ces situations vont passer ou non le vérificateur
d'emprunt avant que vous ne compiliez ; et vérifiez ensuite si vous aviez
raison !
Penser en termes de durées de vie
La façon dont vous avez à préciser les paramètres de durées de vie dépend de ce
que fait votre fonction. Par exemple, si nous changions l'implémentation de la
fonction la_plus_longue
pour qu'elle retourne systématiquement le premier
paramètre plutôt que la slice de chaîne de caractères la plus longue, nous
n'aurions pas besoin de renseigner une durée de vie sur le paramètre y
. Le
code suivant se compile :
Fichier : src/main.rs
fn main() { let string1 = String::from("abcd"); let string2 = "efghijklmnopqrstuvwxyz"; let resultat = la_plus_longue(string1.as_str(), string2); println!("La chaîne la plus longue est {}", resultat); } fn la_plus_longue<'a>(x: &'a str, y: &str) -> &'a str { x }
Dans cet exemple, nous avons précisé un paramètre de durée de vie 'a
sur le
paramètre x
et sur le type de retour, mais pas sur le paramètre y
, car la
durée de vie de y
n'a pas de lien avec la durée de vie de x
ou de la valeur
de retour.
Lorsqu'on retourne une référence à partir d'une fonction, le paramètre de la
durée de vie pour le type de retour doit correspondre à une des durées des
paramètres. Si la référence retournée ne se réfère pas à un de ses paramètres,
elle se réfère probablement à une valeur créée à l'intérieur de cette fonction,
et elle deviendra une référence pendouillante car sa valeur va sortir de la
portée à la fin de la fonction. Imaginons cette tentative d'implémentation de
la fonction la_plus_longue
qui ne se compile pas :
Fichier : src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let resultat = la_plus_longue(string1.as_str(), string2);
println!("La chaîne la plus longue est {}", resultat);
}
fn la_plus_longue<'a>(x: &str, y: &str) -> &'a str {
let resultat = String::from("très longue chaîne");
resultat.as_str()
}
Ici, même si nous avons précisé un paramètre de durée de vie 'a
sur le type de
retour, cette implémentation va échouer à la compilation car la durée de vie de
la valeur de retour n'est pas du tout liée à la durée de vie des paramètres.
Voici le message d'erreur que nous obtenons :
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return reference to local variable `result`
--> src/main.rs:11:5
|
11 | resultat.as_str()
| ^^^^^^^^^^^^^^^^^ returns a reference to data owned by the current function
For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` due to previous error
Le problème est que resultat
sort de la portée et est effacée à la fin de la
fonction la_plus_longue
. Nous avons aussi essayé de retourner une référence
vers resultat
à partir de la fonction. Il n'existe aucune façon d'écrire les
paramètres de durée de vie de telle manière que cela changerait la référence
pendouillante, et Rust ne nous laissera pas créer une référence pendouillante.
Dans notre cas, la meilleure solution consiste à retourner un type de donnée
dont on va prendre possession plutôt qu'une référence, ainsi le code appelant
sera responsable du nettoyage de la valeur.
Enfin, la syntaxe de la durée de vie sert à interconnecter les durées de vie de plusieurs paramètres ainsi que les valeurs de retour des fonctions. Une fois celles-ci interconnectés, Rust a assez d'informations pour autoriser les opérations sécurisées dans la mémoire et refuser les opérations qui pourraient créer des pointeurs pendouillants ou alors enfreindre la sécurité de la mémoire.
L'ajout des durées de vies dans les définitions des structures
Jusqu'à présent, nous avons défini des structures pour contenir des types qui
sont possédés par elles-mêmes. Il est possible qu'une structure puisse contenir
des références, mais dans ce cas nous devons préciser une durée de vie sur
chaque référence dans la définition de la structure. L'encart 10-25 montre une
structure ExtraitImportant
qui stocke une slice de chaîne de caractères.
Fichier : src/main.rs
struct ExtraitImportant<'a> { partie: &'a str, } fn main() { let roman = String::from("Appelez-moi Ismaël. Il y a quelques années ..."); let premiere_phrase = roman.split('.') .next() .expect("Impossible de trouver un '.'"); let i = ExtraitImportant { partie: premiere_phrase }; }
Cette structure a un champ, partie
, qui stocke une slice de chaîne de
caractères, qui est une référence. Comme pour les types de données génériques,
nous déclarons le nom du paramètre de durée de vie générique entre des chevrons
après le nom de la structure pour que nous puissions utiliser le paramètre de
durée de vie dans le corps de la définition de la structure. Cette annotation
signifie qu'une instance de ExtraitImportant
ne peut pas vivre plus longtemps
que la référence qu'elle stocke dans son champ partie
.
La fonction main
crée ici une instance de la structure ExtraitImportant
qui
stocke une référence vers la première phrase de la String
possédée par la
variable roman
. Les données dans roman
existent avant que l'instance de
ExtraitImportant
soit crée. De plus, roman
ne sort pas de la portée avant
que l'instance de ExtraitImportant
sorte de la portée, donc la référence dans
l'instance de ExtraitImportant
est toujours valide.
L'élision des durées de vie
Vous avez appris que toute référence a une durée de vie et que vous devez renseigner des paramètres de durée de vie sur des fonctions ou des structures qui utilisent des références. Cependant, dans le chapitre 4 nous avions une fonction dans l'encart 4-9, qui est montrée à nouveau dans l'encart 10-26, qui compilait sans informations de durée de vie.
Fichier : src/lib.rs
fn premier_mot(s: &str) -> &str { let octets = s.as_bytes(); for (i, &element) in octets.iter().enumerate() { if element == b' ' { return &s[0..i]; } } &s[..] } fn main() { let my_string = String::from("hello world"); // first_word works on slices of `String`s let word = premier_mot(&my_string[..]); let my_string_literal = "hello world"; // first_word works on slices of string literals let word = premier_mot(&my_string_literal[..]); // Because string literals *are* string slices already, // this works too, without the slice syntax! let word = premier_mot(my_string_literal); }
La raison pour laquelle cette fonction se compile sans annotation de durée de vie est historique : dans les premières versions de Rust (avant la 1.0), ce code ne se serait pas compilé parce que chaque référence devait avoir une durée de vie explicite. A l'époque, la signature de la fonction devait être écrite ainsi :
fn premier_mot<'a>(s: &'a str) -> &'a str {
Après avoir écrit une grande quantité de code Rust, l'équipe de Rust s'est rendu compte que les développeurs Rust saisissaient toujours les mêmes durées de vie encore et encore dans des situations spécifiques. Ces situations étaient prévisibles et suivaient des schémas prédéterminés. Les développeurs ont programmé ces schémas dans le code du compilateur afin que le vérificateur d'emprunt puisse deviner les durées de vie dans ces situations et n'auront plus besoin d'annotations explicites.
Cette partie de l'histoire de Rust est intéressante car il est possible que d'autres modèles prédéterminés émergent et soient ajoutés au compilateur. A l'avenir, il est possible qu'encore moins d'annotations de durée de vie soient nécessaires.
Les schémas programmés dans l'analyse des références de Rust s'appellent les règles d'élision des durées de vie. Ce ne sont pas des règles que les développeurs doivent suivre ; c'est un jeu de cas particuliers que le compilateur va essayer de comparer à votre code, et s'il y a une correspondance alors vous n'aurez pas besoin d'écrire explicitement les durées de vie.
Les règles d'élision ne permettent pas de faire des déductions complètes. Si Rust applique les règles de façon stricte, mais qu'il existe toujours une ambiguïté quant à la durée de vie des références, le compilateur ne devinera pas quelle devrait être la durée de vie des autres références. Dans ce cas, au lieu de tenter de deviner, le compilateur va vous afficher une erreur que vous devrez résoudre en précisant les durées de vie qui clarifieront les liens entre chaque référence.
Les durées de vies sur les fonctions ou les paramètres des fonctions sont appelées les durées de vie des entrées, et les durées de vie sur les valeurs de retour sont appelées les durées de vie des sorties.
Le compilateur utilise trois règles pour déterminer quelles devraient être les durées
de vie des références si cela n'est pas indiqué explicitement. La première règle
s'applique sur les durées de vie des entrées, et les deuxième et troisième règles
s'appliquent sur les durées de vie des sorties. Si le compilateur arrive à la
fin des trois règles et qu'il y a encore des références pour lesquelles il ne
peut pas savoir leur durée de vie, le compilateur s'arrête avec une erreur. Ces
règles s'appliquent sur les définitions des fn
ainsi que sur celles des blocs
impl
.
La première règle dit que chaque paramètre qui est une référence a sa propre
durée de vie. Autrement dit, une fonction avec un seul paramètre va avoir un
seul paramètre de durée de vie : fn foo<'a>(x: &'a i32)
; une fonction avec
deux paramètres va avoir deux paramètres de durée de vie séparés :
fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
; et ainsi de suite.
La deuxième règle dit que s'il y a exactement un seul paramètre de durée de vie
d'entrée, cette durée de vie est assignée à tous les paramètres de durée de vie
des sorties : fn foo<'a>(x: &'a i32) -> &'a i32
.
La troisième règle est que lorsque nous avons plusieurs paramètres de durée de
vie, mais qu'un d'entre eux est &self
ou &mut self
parce que c'est une
méthode, la durée de vie de self
sera associée à tous les paramètres de durée
de vie des sorties. Cette troisième règle rend les méthodes plus faciles à lire
et à écrire car il y a moins de caractères nécessaires.
Imaginons que nous soyons le compilateur. Nous allons appliquer ces règles pour
déduire quelles seront les durées de vie des références dans la signature de la
fonction premier_mot
de l'encart 10-26.
fn premier_mot(s: &str) -> &str {
Le compilateur applique alors la première règle, qui dit que chaque référence
a sa propre durée de vie. Appellons-la 'a
comme d'habitude, donc maintenant la
signature devient ceci :
fn premier_mot<'a>(s: &'a str) -> &str {
La deuxième règle s'applique car il y a exactement une durée de vie d'entrée ici. La deuxième règle dit que la durée de vie du seul paramètre d'entrée est affectée à la durée de vie des sorties, donc la signature est maintenant ceci :
fn premier_mot<'a>(s: &'a str) -> &'a str {
Maintenant, toutes les références de cette signature de fonction ont des durées de vie, et le compilateur peut continuer son analyse sans avoir besoin que le développeur renseigne les durées de vie dans cette signature de fonction.
Voyons un autre exemple, qui utilise cette fois la fonction la_plus_longue
qui
n'avait pas de paramètres de durée de vie lorsque nous avons commencé à
l'utiliser dans l'encart 10-21 :
fn la_plus_longue(x: &str, y: &str) -> &str {
Appliquons la première règle : chaque référence a sa propre durée de vie. Cette fois, nous avons avons deux références au lieu d'une seule, donc nous avons deux durées de vie :
fn la_plus_longue<'a, 'b>(x: &'a str, y: &'b str) -> &str {
Vous pouvez constater que la deuxième règle ne s'applique pas car il y a plus
d'une seule durée de vie. La troisième ne s'applique pas non plus, car
la_plus_longue
est une fonction et pas une méthode, donc aucun de ses
paramètres ne sont self
. Après avoir utilisé ces trois règles, nous n'avons
pas pu en déduire la durée de vie de la valeur de retour. C'est pourquoi nous
obtenons une erreur en essayant de compiler le code dans l'encart 10-21 : le
compilateur a utilisé les règles d'élision des durées de vie mais n'est pas
capable d'en déduire toutes les durées de vie des références présentes dans la
signature.
Comme la troisième règle ne s'applique que sur les signatures des méthodes, nous allons examiner les durées de vie dans ce contexte pour comprendre pourquoi la troisième règle signifie que nous n'avons pas souvent besoin d'annoter les durées de vie dans les signatures des méthodes.
Informations de durée de vie dans les définitions des méthodes
Lorsque nous implémentons des méthodes sur une structure avec des durées de vie, nous utilisons la même syntaxe que celle des paramètres de type génériques que nous avons vue dans l'encart 10-11. L'endroit où nous déclarons et utilisons les paramètres de durée de vie dépend de s'ils sont reliés aux champs des structures ou aux paramètres de la méthode et aux valeurs de retour.
Les noms des durées de vie pour les champs de structure ont toujours besoin
d'être déclarés après le mot-clé impl
et sont ensuite utilisés après le nom de
la structure, car ces durées vie font partie du type de la structure.
Sur les signatures des méthodes à l'intérieur du bloc impl
, les références
peuvent être liées à la durée de vie des références de champs de la structure, ou
elles peuvent être indépendantes. De plus, les règles d'élision des durées de
vie font parfois en sorte que l'ajout de durées de vie n'est parfois pas
nécessaire dans les signatures des méthodes. Voyons quelques exemples en
utilisant la structure ExtraitImportant
que nous avons définie dans l'encart
10-25.
Premièrement, nous allons utiliser une méthode niveau
dont le seul paramètre
est une référence à self
et dont la valeur de retour sera un i32
, qui n'est
pas une référence :
struct ExtraitImportant<'a> { partie: &'a str, } impl<'a> ExtraitImportant<'a> { fn niveau(&self) -> i32 { 3 } } impl<'a> ExtraitImportant<'a> { fn annoncer_et_retourner_partie(&self, annonce: &str) -> &str { println!("Votre attention s'il vous plaît : {}", annonce); self.partie } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ExtraitImportant { partie: first_sentence, }; }
La déclaration du paramètre de durée de vie après impl
et son utilisation
après le nom du type sont nécessaires, mais nous n'avons pas à préciser la durée
de vie de la référence à self
grâce à la première règle d'élision.
Voici un exemple où la troisième règle d'élision des durées de vie s'applique :
struct ExtraitImportant<'a> { partie: &'a str, } impl<'a> ExtraitImportant<'a> { fn niveau(&self) -> i32 { 3 } } impl<'a> ExtraitImportant<'a> { fn annoncer_et_retourner_partie(&self, annonce: &str) -> &str { println!("Votre attention s'il vous plaît : {}", annonce); self.partie } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ExtraitImportant { partie: first_sentence, }; }
Il y a deux durées de vies des entrées, donc Rust applique la première règle
d'élision des durées de vie et donne à &self
et annonce
leur
propre durée de vie. Ensuite, comme un des paramètres est &self
, le type de
retour obtient la durée de vie de &self
, de sorte que toutes les durées de
vie ont été calculées.
La durée de vie statique
Une durée de vie particulière que nous devons aborder est 'static
, qui
signifie que cette référence peut vivre pendant la totalité de la durée du
programme. Tous les littéraux de chaînes de caractères ont la durée de vie
'static
, que nous pouvons écrire comme ceci :
#![allow(unused)] fn main() { let s: &'static str = "J'ai une durée de vie statique."; }
Le texte de cette chaîne de caractères est stocké directement dans le binaire du
programme, qui est toujours disponible. C'est pourquoi la durée de vie de tous
les littéraux de chaînes de caractères est 'static
.
Il se peut que voyiez des suggestions pour utiliser la durée de vie 'static
dans les messages d'erreur. Mais avant d'utiliser 'static
comme durée de vie
pour une référence, demandez-vous si la référence en question vit bien pendant
toute la vie de votre programme, ou non. Vous devriez vous demander si vous
voulez qu'elle vive aussi longtemps, même si si c'était possible. La plupart du
temps, le problème résulte d'une tentative de création d'une référence
pendouillante ou d'une inadéquation des durées de vie disponibles. Dans ces
cas-là, la solution consiste à résoudre ces problèmes, et pas à renseigner la
durée de vie comme étant 'static
.
Les paramètres de type génériques, les traits liés, et les durées de vies ensemble
Regardons brièvement la syntaxe pour renseigner tous les paramètres de type génériques, les traits liés, et les durées de vies sur une seule fonction !
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let resultat = la_plus_longue_avec_annonce( string1.as_str(), string2, "Aujourd'hui, c'est l'anniversaire de quelqu'un !", ); println!("La chaîne la plus longue est {}", resultat); } use std::fmt::Display; fn la_plus_longue_avec_annonce<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str where T: Display { println!("Annonce ! {}", ann); if x.len() > y.len() { x } else { y } }
C'est la fonction la_plus_longue
de l'encart 10-22 qui retourne la plus grande
de deux slices de chaînes de caractères. Mais maintenant elle a un paramètre
supplémentaire ann
de type générique T
, qui peut être remplacé par n'importe
quel type qui implémente le trait Display
comme le précise la clause where
.
Ce paramètre supplémentaire sera affiché avec {}
, c'est pourquoi le trait lié
Display
est nécessaire. Comme les durées de vie sont un type de génériques,
les déclarations du paramètre de durée de vie 'a
et le paramètre de type
générique T
vont dans la même liste à l'intérieur des chevrons après le nom de
la fonction.
Résumé
Nous avons vu beaucoup de choses dans ce chapitre ! Maintenant que vous en savez plus sur les paramètres de type génériques, les traits et les traits liés, ainsi que sur les paramètres de durée de vie génériques, vous pouvez maintenant écrire du code en évitant les doublons qui va bien fonctionner dans de nombreuses situations. Les paramètres de type génériques vous permettent d'appliquer du code à différents types. Les traits et les traits liés s'assurent que bien que les types soient génériques, ils auront un comportement particulier sur lequel le code peut compter. Vous avez appris comment utiliser les indications de durée de vie pour s'assurer que ce code flexible n'aura pas de références pendouillantes. Et toutes ces vérifications se font au moment de la compilation, ce qui n'influe pas sur les performances au moment de l'exécution du programme !
Croyez-le ou non, mais il y a encore des choses à apprendre sur les sujets que nous avons traités dans ce chapitre : le chapitre 17 expliquera les objets de trait, qui est une façon d'utiliser les traits. Il existe aussi des situations plus complexes impliquant des indications de durée de vie dont vous n'aurez besoin que dans certains cas de figure très avancés; pour ces cas-là, vous devriez consulter la Référence de Rust. Maintenant, nous allons voir au chapitre suivant comment écrire des tests en Rust afin que vous puissiez vous assurer que votre code fonctionne comme il devrait le faire.