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

Valider les références avec les durées de vie

Les durées de vie sont un autre type de générique que nous avons déjà utilisé. Plutôt que de s’assurer qu’un type à le comportement que nous souhaitons, les durées de vie s’assurent que les références sont valides aussi longtemps que nous en avons besoin.

Un détail que nous n’avons pas abordé dans la section « Les références et l’emprunt » du chapitre 4 est que chaque référence en Rust à une durée de vie, qui est la portée dans laquelle cette référence est valide. La plupart du temps, les durées de vie sont implicites et inférées, tout comme la plupart du temps, les types sont inférés. Nous ne devons annoter explicitement les types que lorsque plusieurs types sont possibles. De la même manière, nous devons annoter les durées de vie lorsque les durées de vie des références peuvent être liées de plusieurs façons différentes. Rust nous demande d’annoter les relations en utilisant des paramètres génériques de durée de vie pour s’assurer que les références réelles utilisées à l’exécution seront certainement valides.

Annoter les durées de vie n’est même pas un concept que la plupart des autres langages de programmation possèdent, donc cela va sembler inhabituel. Bien que nous ne couvrirons pas les durées de vie dans leur intégralité dans ce chapitre, nous aborderons les façons courantes dont vous pourriez rencontrer la syntaxe des durées de vie afin que vous puissiez vous familiariser avec le concept.

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

L’objectif principal des durées de vie est d’empêcher les références pendantes (dangling références) qui, si elles étaient autorisées à exister, amèneraient un programme à référencer des données autres que celles qu’il est censé référencer. Considérez le programme de l’encart 10-16, qui à une portée externe et une portée interne.

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {r}");
}
Listing 10-16: An attempt to use a reference whose value has gone out of scope

Remarque : les exemples des encarts 10-16, 10-17 et 10-23 déclarent des variables sans leur donner de valeur initiale, de sorte que le nom de la variable existe dans la portée externe. À première vue, cela pourrait sembler en conflit avec le fait que Rust n’a pas de valeurs nulles. Cependant, si nous essayons d’utiliser une variable avant de lui donner une valeur, nous obtiendrons une erreur de compilation, ce qui montre qu’effectivement Rust n’autorise pas les valeurs nulles.

La portée externe déclare une variable nommée r sans valeur initiale, et la portée interne déclare une variable nommée x avec la valeur initiale 5. À l’intérieur de la portée interne, nous tentons de définir la valeur de r comme une référence vers x. Ensuite, la portée interne se terminé, et nous tentons d’afficher la valeur dans r. Ce code ne compilera pas, car la valeur à laquelle r fait référence est sortie de la portée avant que nous n’essayions de l’utiliser. Voici le message d’erreur : console {{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-16/output.txt}}

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
5 |         let x = 5;
  |             - binding `x` declared here
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     println!("r: {r}");
  |                   - borrow later used here

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

Le message d’erreur dit que la variable x « ne vit pas assez longtemps ». La raison est que x sera hors de portée lorsque la portée interne se terminera à la ligne 7. Mais r est encore valide pour la portée externe ; comme sa portée est plus grande, nous disons qu’elle « vit plus longtemps ». Si Rust autorisait ce code à fonctionner, r référencerait de la mémoire qui a été désallouée lorsque x est sorti de la portée, et tout ce que nous essaierions de faire avec r ne fonctionnerait pas correctement. Alors, comment Rust détermine-t-il que ce code est invalide ? Il utilise le vérificateur d’emprunt (borrow checker).

Le vérificateur d’emprunt

Le compilateur Rust possède un vérificateur d’emprunt (borrow checker) qui compare les portées pour déterminer si tous les emprunts sont valides. L’encart 10-17 montre le même code que l’encart 10-16 mais avec des annotations montrant les durées de vie des variables.

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}                         // ---------+
Listing 10-17: Annotations of the lifetimes of r and x, named 'a and 'b, respectively

Ici, nous avons annoté la durée de vie de r avec 'a et la durée de vie de x avec 'b. Comme vous pouvez le voir, le bloc interne 'b est beaucoup plus petit que le bloc de durée de vie externe 'a. Au moment de la compilation, Rust compare la taille des deux durées de vie et constate que r à une durée de vie de 'a mais qu’il fait référence à de la mémoire avec une durée de vie de 'b. Le programme est rejeté car 'b est plus court que 'a : le sujet de la référence ne vit pas aussi longtemps que la référence.

L’encart 10-18 corrige le code pour qu’il n’ait pas de référence pendante et qu’il compilé sans aucune erreur.

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {r}");   //   |       |
                          // --+       |
}                         // ----------+
Listing 10-18: A valid reference because the data has a longer lifetime than the reference

Ici, x à la durée de vie 'b, qui dans ce cas est plus grande que 'a. Cela signifie que r peut référencer x car Rust sait que la référence dans r sera toujours valide tant que x est valide.

Maintenant que vous savez où se trouvent les durées de vie des références et comment Rust analyse les durées de vie pour s’assurer que les références seront toujours valides, explorons les durées de vie génériques dans les paramètres de fonction et les valeurs de retour.

Les durées de vie génériques dans les fonctions

Nous allons écrire une fonction qui retourné la plus longue de deux slices de chaînes de caractères. Cette fonction prendra deux slices de chaînes de caractères et retournera une seule slice de chaîne de caractères. Après avoir implémenté la fonction longest, le code de l’encart 10-19 devrait afficher The longest string is abcd.

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}
Listing 10-19: A main function that calls the longest function to find the longer of two string slices

Notez que nous voulons que la fonction prenne des slices de chaînes de caractères, qui sont des références, plutôt que des chaînes de caractères, car nous ne voulons pas que la fonction longest prenne la possession de ses paramètres. Reportez-vous à [« Les slices de chaînes de caractères comme paramètres »][string-slices-as-parameters] au chapitre 4 pour plus de discussion sur les raisons pour lesquelles les paramètres que nous utilisons dans l’encart 10-19 sont ceux que nous souhaitons.

Si nous essayons d’implémenter la fonction longest comme montré dans l’encart 10-20, elle ne compilera pas.

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-20: An implementation of the longest function that returns the longer of two string slices but does not yet compile

À la place, nous obtenons l’erreur suivante qui parle de durées de vie : console {{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-20/output.txt}}

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(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 longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

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

Le texte d’aide révèle que le type de retour a besoin d’un paramètre de durée de vie générique car Rust ne peut pas déterminer si la référence retournée fait référence à x ou à y. En fait, nous ne le savons pas non plus, car le bloc if dans le corps de cette fonction retourné une référence vers x et le bloc else retourné une référence vers y !

Lorsque nous définissons cette fonction, nous ne connaissons pas les valeurs concrètes qui seront passées à cette fonction, donc nous ne savons pas si le cas if ou le cas else s’exécutera. Nous ne connaissons pas non plus les durées de vie concrètes des références qui seront passées, donc nous ne pouvons pas examiner les portées comme nous l’avons fait dans les encarts 10-17 et 10-18 pour déterminer si la référence que nous retournons sera toujours valide. Le vérificateur d’emprunt ne peut pas non plus le déterminer, car il ne sait pas comment les durées de vie de x et y sont liées à la durée de vie de la valeur de retour. Pour corriger cette erreur, nous ajouterons 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 effectuer son analyse.

La syntaxe des annotations de durée de vie

Les annotations de durée de vie ne changent pas la durée de vie d’une référence. Elles décrivent plutôt les relations entre les durées de vie de plusieurs références sans affecter les durées de vie elles-mêmes. Tout comme les fonctions peuvent accepter n’importe quel type lorsque la signature spécifie 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 spécifiant un paramètre de durée de vie générique.

Les annotations de durée de vie ont une syntaxe légèrement inhabituelle : les noms des paramètres de durée de vie doivent commencer par une apostrophe (') et sont généralement tout en minuscules et très courts, comme les types génériques. La plupart des gens utilisent le nom 'a pour la première annotation de durée de vie. Nous plaçons les annotations de paramètre de durée de vie 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 vers un i32 sans paramètre de durée de vie, une référence vers un i32 qui à un paramètre de durée de vie nommé 'a, et une référence mutable vers un i32 qui a aussi la durée de vie 'a :

&i32        // a référence
&'a i32     // a référence with an explicit lifetime
&'a mut i32 // a mutable référence with an explicit lifetime

Une seule annotation de durée de vie n’a pas beaucoup de sens en elle-même, car les annotations sont destinées à indiquer à Rust comment les paramètres de durée de vie génériques de plusieurs références sont liés les uns aux autres. Examinons comment les annotations de durée de vie sont liées les unes aux autres dans le contexte de la fonction longest.

Dans les signatures de fonctions

Pour utiliser les annotations de durée de vie dans les signatures de fonctions, nous devons déclarer les paramètres de durée de vie génériques entre chevrons entre le nom de la fonction et la liste des paramètres, tout comme nous l’avons fait avec les paramètres de type générique.

Nous voulons que la signature exprime la contrainte suivante : la référence retournée sera valide tant que les deux paramètres sont valides. C’est la relation entre les durées de vie des paramètres et la valeur de retour. Nous nommerons la durée de vie 'a puis l’ajouterons à chaque référence, comme montré dans l’encart 10-21.

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-21: The longest function definition specifying that all the references in the signature must have the same lifetime 'a

Ce code devrait compiler et produire le résultat souhaité lorsque nous l’utilisons avec la fonction main de l’encart 10-19.

La signature de la fonction indique maintenant à Rust que pour une certaine durée de vie 'a, la fonction prend deux paramètres, qui sont tous les deux des slices de chaînes de caractères qui vivent au moins aussi longtemps que la durée de vie 'a. La signature de la fonction indique aussi à Rust que la slice de chaîne de caractères retournée par la fonction vivra au moins aussi longtemps que la durée de vie 'a. En pratique, cela signifie que la durée de vie de la référence retournée par la fonction longest est la même que la plus petite des durées de vie des valeurs référencées par les arguments de la fonction. Ce sont ces relations que nous voulons que Rust utilise lors de l’analyse de ce code.

Rappelez-vous, lorsque nous spécifions les paramètres de durée de vie dans cette signature de fonction, nous ne changeons pas les durées de vie des valeurs passées ou retournées. Nous spécifions plutôt que le vérificateur d’emprunt doit rejeter toute valeur qui ne respecte pas ces contraintes. Notez que la fonction longest n’a pas besoin de savoir exactement combien de temps x et y vivront, seulement qu’une certaine portée peut être substituée à 'a qui satisfera cette signature.

Lorsque nous annotons les durées de vie dans les fonctions, les annotations vont dans la signature de la fonction, pas dans le corps de la fonction. Les annotations de durée de vie font partie du contrat de la fonction, tout comme les types dans la signature. Avoir des signatures de fonctions contenant le contrat de durée de vie signifie que l’analyse effectuée par le compilateur Rust peut être plus simple. S’il y à un problème avec la façon dont une fonction est annotée ou la façon dont elle est appelée, les erreurs du compilateur peuvent pointer plus précisément vers la partie de notre code et les contraintes. Si, au contraire, le compilateur Rust faisait plus d’inférences sur ce que nous voulions que soient les relations entre les durées de vie, le compilateur ne pourrait peut-être pointer que vers une utilisation de notre code à plusieurs étapes de la cause du problème.

Lorsque nous passons des références concrètes à longest, la durée de vie concrète qui est substituée à 'a est la partie de la portée de x qui chevauche la portée de y. En d’autres termes, la durée de vie générique 'a obtiendra la durée de vie concrète qui est égale à la plus petite des durées de vie de x et y. Comme nous avons annoté 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 aussi valide pour la durée de la plus petite des durées de vie de x et y.

Voyons comment les annotations de durée de vie restreignent la fonction longest en passant des références qui ont des durées de vie concrètes différentes. L’encart 10-22 est un exemple simple.

Filename: src/main.rs
fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {result}");
    }
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-22: Using the longest function with references to String values that have different concrete lifetimes

Dans cet exemple, string1 est valide jusqu’à la fin de la portée externe, string2 est valide jusqu’à la fin de la portée interne, et result référence quelque chose qui est valide jusqu’à la fin de la portée interne. Exécutez ce code et vous verrez que le vérificateur d’emprunt approuve ; il compilera et affichera The longest string is long string is long.

Ensuite, essayons un exemple qui montre que la durée de vie de la référence dans result doit être la plus petite durée de vie des deux arguments. Nous déplacerons la déclaration de la variable result à l’extérieur de la portée interne mais laisserons l’assignation de la valeur à la variable result à l’intérieur de la portée avec string2. Ensuite, nous déplacerons le println! qui utilise result à l’extérieur de la portée interne, après la fin de la portée interne. Le code de l’encart 10-23 ne compilera pas.

Filename: src/main.rs
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {result}");
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Listing 10-23: Attempting to use result after string2 has gone out of scope

Lorsque nous essayons de compiler ce code, nous obtenons cette erreur : console {{#include ../listings/ch10-generic-types-traits-and-lifetimes/listing-10-23/output.txt}}

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
5 |         let string2 = String::from("xyz");
  |             ------- binding `string2` declared here
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {result}");
  |                                      ------ borrow later used here

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

L’erreur montre que pour que result soit valide pour l’instruction println!, string2 devrait être valide jusqu’à la fin de la portée externe. Rust le sait car nous avons annoté les durées de vie des paramètres de fonction et des valeurs de retour en utilisant le même paramètre de durée de vie 'a.

En tant qu’humains, nous pouvons regarder ce code et voir que string1 est plus longue que string2, et donc, result contiendra une référence vers string1. Comme string1 n’est pas encore sortie de la portée, une référence vers string1 sera toujours valide pour l’instruction println!. Cependant, le compilateur ne peut pas voir que la référence est valide dans ce cas. Nous avons dit à Rust que la durée de vie de la référence retournée par la fonction longest est la même que la plus petite des durées de vie des références passées. Par conséquent, le vérificateur d’emprunt interdit le code de l’encart 10-23 comme pouvant avoir une référence invalide.

Essayez de concevoir d’autres expériences qui font varier les valeurs et les durées de vie des références passées à la fonction longest et la façon dont la référence retournée est utilisée. Faites des hypothèses sur le fait que vos expériences passeront ou non le vérificateur d’emprunt avant de compiler ; puis, vérifiez si vous avez raison !

Les relations

La façon dont vous devez spécifier les paramètres de durée de vie dépend de ce que fait votre fonction. Par exemple, si nous changions l’implémentation de la fonction longest pour toujours retourner le premier paramètre plutôt que la plus longue slice de chaîne de caractères, nous n’aurions pas besoin de spécifier une durée de vie sur le paramètre y. Le code suivant compilera :

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "efghijklmnopqrstuvwxyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

Nous avons spécifié un paramètre de durée de vie 'a pour le paramètre x et le type de retour, mais pas pour le paramètre y, car la durée de vie de y n’à aucune relation avec la durée de vie de x ou la valeur de retour.

Lorsqu’on retourné une référence depuis une fonction, le paramètre de durée de vie pour le type de retour doit correspondre au paramètre de durée de vie de l’un des paramètres. Si la référence retournée ne fait pas référence à l’un des paramètres, elle doit faire référence à une valeur créée dans cette fonction. Cependant, ce serait une référence pendante car la valeur sortira de la portée à la fin de la fonction. Considérez cette tentative d’implémentation de la fonction longest qui ne compilera pas :

Filename: src/main.rs
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

Ici, même si nous avons spécifié un paramètre de durée de vie 'a pour le type de retour, cette implémentation ne compilera pas 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 : console {{#include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-09-unrelated-lifetime/output.txt}}

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ------^^^^^^^^^
   |     |
   |     returns a value referencing data owned by the current function
   |     `result` is borrowed here

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

Le problème est que result sort de la portée et est nettoyé à la fin de la fonction longest. Nous essayons aussi de retourner une référence vers result depuis la fonction. Il n’y à aucun moyen de spécifier des paramètres de durée de vie qui changeraient la référence pendante, et Rust ne nous laissera pas créer une référence pendante. Dans ce cas, la meilleure solution serait de retourner un type de données possédé plutôt qu’une référence afin que la fonction appelante soit alors responsable du nettoyage de la valeur.

En fin de compte, la syntaxe des durées de vie consiste à connecter les durées de vie des différents paramètres et valeurs de retour des fonctions. Une fois qu’elles sont connectées, Rust a suffisamment d’informations pour autoriser les opérations sûres pour la mémoire et interdire les opérations qui créeraient des pointeurs pendants ou violeraient autrement la sécurité de la mémoire.

Dans les définitions de structs

Jusqu’à présent, les structs que nous avons définies contiennent toutes des types possédés. Nous pouvons définir des structs qui contiennent des références, mais dans ce cas, nous devrions ajouter une annotation de durée de vie sur chaque référence dans la définition de la struct. L’encart 10-24 possède une struct nommée ImportantExcerpt qui contient une slice de chaîne de caractères.

Filename: src/main.rs
struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}
Listing 10-24: A struct that holds a reference, requiring a lifetime annotation

Cette struct à un seul champ part qui contient 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 chevrons après le nom de la struct afin de pouvoir utiliser le paramètre de durée de vie dans le corps de la définition de la struct. Cette annotation signifie qu’une instance de ImportantExcerpt ne peut pas survivre à la référence qu’elle contient dans son champ part.

La fonction main ici crée une instance de la struct ImportantExcerpt qui contient une référence vers la première phrase du String possédé par la variable novel. Les données dans novel existent avant que l’instance de ImportantExcerpt ne soit créée. De plus, novel ne sort pas de la portée avant que ImportantExcerpt ne sorte de la portée, donc la référence dans l’instance de ImportantExcerpt est valide.

L’élision des durées de vie

Vous avez appris que chaque référence à une durée de vie et que vous devez spécifier des paramètres de durée de vie pour les fonctions ou structs qui utilisent des références. Cependant, nous avions une fonction dans l’encart 4-9, montrée à nouveau dans l’encart 10-25, qui compilait sans annotations de durée de vie.

Filename: src/lib.rs
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == 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 = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word works on slices of string literals
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}
Listing 10-25: A function we defined in Listing 4-9 that compiled without lifetime annotations, even though the parameter and return type are references

La raison pour laquelle cette fonction compilé sans annotations de durée de vie est historique : dans les premières versions (pré-1.0) de Rust, ce code n’aurait pas compilé, car chaque référence nécessitait une durée de vie explicite. À cette époque, la signature de la fonction aurait été écrite comme ceci :

fn first_word<'a>(s: &'a str) -> &'a str {

Après avoir écrit beaucoup de code Rust, l’équipe Rust a constaté que les programmeurs Rust saisissaient les mêmes annotations de durée de vie encore et encore dans des situations particulières. Ces situations étaient prévisibles et suivaient quelques modèles déterministes. Les développeurs ont programmé ces modèles dans le code du compilateur afin que le vérificateur d’emprunt puisse inférer les durées de vie dans ces situations sans avoir besoin d’annotations explicites.

Ce morceau d’histoire de Rust est pertinent car il est possible que d’autres modèles déterministes émergent et soient ajoutés au compilateur. À l’avenir, encore moins d’annotations de durée de vie pourraient être nécessaires.

Les modèles programmés dans l’analyse des références par Rust sont appelés les règles d’élision des durées de vie. Ce ne sont pas des règles que les programmeurs doivent suivre ; ce sont un ensemble de cas particuliers que le compilateur considérera, et si votre code correspond à ces cas, vous n’avez pas besoin d’écrire les durées de vie explicitement.

Les règles d’élision ne fournissent pas une inférence complète. S’il reste de l’ambiguïté sur les durées de vie des références après que Rust a appliqué les règles, le compilateur ne devinera pas quelle devrait être la durée de vie des références restantes. Au lieu de deviner, le compilateur vous donnera une erreur que vous pouvez résoudre en ajoutant les annotations de durée de vie.

Les durées de vie sur les paramètres de fonction ou de méthode sont appelées durées de vie d’entrée, et les durées de vie sur les valeurs de retour sont appelées durées de vie de sortie.

Le compilateur utilise trois règles pour déterminer les durées de vie des références lorsqu’il n’y a pas d’annotations explicites. La première règle s’applique aux durées de vie d’entrée, et les deuxième et troisième règles s’appliquent aux durées de vie de sortie. Si le compilateur arrive à la fin des trois règles et qu’il reste des références pour lesquelles il ne peut pas déterminer les durées de vie, le compilateur s’arrêtera avec une erreur. Ces règles s’appliquent aux définitions fn ainsi qu’aux blocs impl.

La première règle est que le compilateur assigne un paramètre de durée de vie à chaque paramètre qui est une référence. En d’autres termes, une fonction avec un paramètre obtient un paramètre de durée de vie : fn foo<'a>(x: &'a i32) ; une fonction avec deux paramètres obtient 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 est que, s’il y a exactement un 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 de sortie : fn foo<'a>(x: &'a i32) -> &'a i32.

La troisième règle est que, s’il y à plusieurs paramètres de durée de vie d’entrée, mais que l’un d’eux est &self ou &mut self parce qu’il s’agit d’une méthode, la durée de vie de self est assignée à tous les paramètres de durée de vie de sortie. Cette troisième règle rend les méthodes beaucoup plus agréables à lire et à écrire car moins de symboles sont nécessaires.

Faisons comme si nous étions le compilateur. Nous appliquerons ces règles pour déterminer les durées de vie des références dans la signature de la fonction first_word de l’encart 10-25. La signature commence sans aucune durée de vie associée aux références :

fn first_word(s: &str) -> &str {

Ensuite, le compilateur applique la première règle, qui spécifie que chaque paramètre obtient sa propre durée de vie. Nous l’appellerons 'a comme d’habitude, donc maintenant la signature est celle-ci :

fn first_word<'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. La deuxième règle spécifie que la durée de vie du seul paramètre d’entrée est assignée à la durée de vie de sortie, donc la signature est maintenant celle-ci :

fn first_word<'a>(s: &'a str) -> &'a str {

Maintenant, toutes les références dans cette signature de fonction ont des durées de vie, et le compilateur peut continuer son analyse sans avoir besoin que le programmeur annote les durées de vie dans cette signature de fonction.

Regardons un autre exemple, cette fois en utilisant la fonction longest qui n’avait pas de paramètres de durée de vie lorsque nous avons commencé à travailler avec dans l’encart 10-20 :

fn longest(x: &str, y: &str) -> &str {

Appliquons la première règle : chaque paramètre obtient sa propre durée de vie. Cette fois nous avons deux paramètres au lieu d’un, donc nous avons deux durées de vie :

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

Vous pouvez voir que la deuxième règle ne s’applique pas, car il y a plus d’une durée de vie d’entrée. La troisième règle ne s’applique pas non plus, car longest est une fonction plutôt qu’une méthode, donc aucun des paramètres n’est self. Après avoir appliqué les trois règles, nous n’avons toujours pas déterminé quelle est la durée de vie du type de retour. C’est pourquoi nous avons obtenu une erreur en essayant de compiler le code de l’encart 10-20 : le compilateur a appliqué les règles d’élision des durées de vie mais n’a toujours pas pu déterminer toutes les durées de vie des références dans la signature.

Comme la troisième règle ne s’applique vraiment que dans les signatures de méthodes, nous examinerons les durées de vie dans ce contexte ensuite pour voir pourquoi la troisième règle fait que nous n’avons pas à annoter les durées de vie dans les signatures de méthodes très souvent.

Dans les définitions de méthodes

Lorsque nous implémentons des méthodes sur une struct avec des durées de vie, nous utilisons la même syntaxe que celle des paramètres de type générique, comme montré 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 liés aux champs de la struct ou aux paramètres et valeurs de retour de la méthode.

Les noms de durée de vie pour les champs de la struct doivent toujours être déclarés après le mot-clé impl puis utilisés après le nom de la struct car ces durées de vie font partie du type de la struct.

Dans les signatures de 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 dans les champs de la struct, ou elles peuvent être indépendantes. De plus, les règles d’élision des durées de vie font souvent en sorte que les annotations de durée de vie ne soient pas nécessaires dans les signatures de méthodes. Examinons quelques exemples en utilisant la struct nommée ImportantExcerpt que nous avons définie dans l’encart 10-24.

D’abord, nous utiliserons une méthode nommée level dont le seul paramètre est une référence vers self et dont la valeur de retour est un i32, qui n’est pas une référence vers quoi que ce soit : rust {{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-10-lifetimes-on-methods/src/main.rs:1st}}

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: 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 requises, mais en raison de la première règle d’élision, nous ne sommes pas tenus d’annoter la durée de vie de la référence vers self.

Voici un exemple où la troisième règle d’élision des durées de vie s’applique : rust {{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-10-lifetimes-on-methods/src/main.rs:3rd}}

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

Il y a deux durées de vie d’entrée, donc Rust applique la première règle d’élision des durées de vie et donne à &self et à announcement leurs propres durées de vie. Ensuite, comme l’un des paramètres est &self, le type de retour obtient la durée de vie de &self, et toutes les durées de vie ont été prises en compte.

La durée de vie statique

Une durée de vie spéciale que nous devons aborder est 'static, qui indique que la référence concernée peut vivre pendant toute la durée du programme. Toutes les chaînes de caractères littérales ont la durée de vie 'static, que nous pouvons annoter comme suit :

#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}

Le texte de cette chaîne de caractères est stocké directement dans le binaire du programme, qui est toujours disponible. Par conséquent, la durée de vie de toutes les chaînes de caractères littérales est 'static.

Vous pourriez voir des suggestions dans les messages d’erreur pour utiliser la durée de vie 'static. Mais avant de spécifier 'static comme durée de vie pour une référence, demandez-vous si la référence que vous avez vit réellement pendant toute la durée de vie de votre programme, et si vous le souhaitez. La plupart du temps, un message d’erreur suggérant la durée de vie 'static résulte d’une tentative de création d’une référence pendante ou d’une incompatibilité des durées de vie disponibles. Dans de tels cas, la solution est de corriger ces problèmes, pas de spécifier la durée de vie 'static.

Paramètres de type générique, trait bounds et durées de vie

Examinons brièvement la syntaxe pour spécifier des paramètres de type générique, des trait bounds et des durées de vie, le tout dans une seule fonction ! rust {{#rustdoc_include ../listings/ch10-generic-types-traits-and-lifetimes/no-listing-11-generics-traits-and-lifetimes/src/main.rs:here}}

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest_with_an_announcement(
        string1.as_str(),
        string2,
        "Today is someone's birthday!",
    );
    println!("The longest string is {result}");
}

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {ann}");
    if x.len() > y.len() { x } else { y }
}

C’est la fonction longest de l’encart 10-21 qui retourné la plus longue de deux slices de chaînes de caractères. Mais maintenant elle à un paramètre supplémentaire nommé ann du type générique T, qui peut être rempli par n’importe quel type qui implémente le trait Display comme spécifié par la clause where. Ce paramètre supplémentaire sera affiché en utilisant {}, c’est pourquoi le trait bound Display est nécessaire. Comme les durées de vie sont un type de générique, les déclarations du paramètre de durée de vie 'a et du paramètre de type générique T vont dans la même liste entre chevrons après le nom de la fonction.

Résumé

Nous avons couvert beaucoup de choses dans ce chapitre ! Maintenant que vous connaissez les paramètres de type générique, les traits et les trait bounds, et les paramètres de durée de vie génériques, vous êtes prêt à écrire du code sans répétition qui fonctionne dans de nombreuses situations différentes. Les paramètres de type générique vous permettent d’appliquer le code à différents types. Les traits et les trait bounds s’assurent que même si les types sont génériques, ils auront le comportement dont le code a besoin. Vous avez appris à utiliser les annotations de durée de vie pour vous assurer que ce code flexible n’aura pas de références pendantes. Et toute cette analyse se fait au moment de la compilation, ce qui n’affecte pas les performances à l’exécution !

Croyez-le où non, il y a encore beaucoup à apprendre sur les sujets que nous avons abordés dans ce chapitre : le chapitre 18 traite des objets trait, qui sont une autre façon d’utiliser les traits. Il existe aussi des scénarios plus complexes impliquant des annotations de durée de vie dont vous n’aurez besoin que dans des cas très avancés ; pour ceux-là, vous devriez lire la [Référence Rust][référence]. Mais ensuite, vous apprendrez à écrire des tests en Rust pour vous assurer que votre code fonctionne comme il le devrait.