Validation des références avec des durées de vie

RustRustBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Bienvenue dans Validating References With Lifetimes. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous allons discuter des durées de vie et de la manière dont elles assurent que les références sont valides aussi longtemps qu'il est nécessaire. Bien que les durées de vie puissent sembler inconnues, nous allons aborder les façons courantes dont vous pourriez rencontrer la syntaxe des durées de vie pour vous aider à vous sentir à l'aise avec le concept.

Validating References with Lifetimes

Les durées de vie sont un autre type de paramètres génériques que nous avons déjà utilisé. Au lieu de garantir qu'un type a le comportement que nous voulons, les durées de vie garantissent que les références sont valides aussi longtemps que nous en avons besoin.

Un détail que nous n'avons pas discuté dans "References and Borrowing" est que chaque référence en Rust a une durée de vie, qui est la portée pendant 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 devons annoter les types seulement lorsqu'il est possible d'avoir plusieurs types. De manière similaire, nous devons annoter les durées de vie lorsque les durées de vie des références peuvent être liées de plusieurs manières différentes. Rust nous oblige à annoter les relations en utilisant des paramètres de durée de vie génériques pour vous pouvez être certain que les références réelles utilisées au moment de l'exécution seront définitivement valides.

Annoter les durées de vie n'est même pas un concept que la plupart des autres langages de programmation ont, donc cela va sembler inconnu. Bien que nous ne couvrions pas les durées de vie dans leur totalité dans ce chapitre, nous allons discuter des façons courantes dont vous pourriez rencontrer la syntaxe des durées de vie pour que vous puissiez vous sentir à l'aise avec le concept.

Preventing Dangling References with Lifetimes

Le principal objectif des durées de vie est de prévenir les références faussaires, qui entraînent qu'un programme référence des données autres que celles qu'il est censé référencer. Considérez le programme de la Liste 10-16, qui a une portée externe et une portée interne.

fn main() {
  1 let r;

    {
      2 let x = 5;
      3 r = &x;
  4 }

  5 println!("r: {r}");
}

Liste 10-16: Tentative d'utilisation d'une référence dont la valeur est sortie de portée

Note: Les exemples des Listes 10-16, 10-17 et 10-23 déclarent des variables sans leur donner de valeur initiale, de sorte que le nom de variable existe dans la portée externe. Au premier abord, cela peut sembler être 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 que Rust ne permet effectivement pas les valeurs nulles.

La portée externe déclare une variable nommée r sans valeur initiale [1], et la portée interne déclare une variable nommée x avec la valeur initiale de 5 [2]. À l'intérieur de la portée interne, nous tentons de définir la valeur de r comme une référence à x [3]. Ensuite, la portée interne se termine [4], et nous tentons d'afficher la valeur de r [5]. Ce code ne compilera pas car la valeur à laquelle r fait référence est sortie de portée avant que nous n'essayions de l'utiliser. Voici le message d'erreur :

error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
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

Le message d'erreur indique 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 toujours valide pour la portée externe ; parce que sa portée est plus grande, nous disons qu'elle "vit plus longtemps". Si Rust autorisait ce code à fonctionner, r ferait référence à une mémoire qui aurait été désallouée lorsque x est sorti de portée, et tout ce que nous aurions essayé de faire avec r ne fonctionnerait pas correctement. Alors, comment Rust détermine-t-il que ce code est invalide? Il utilise un vérificateur d'emprunt.

The Borrow Checker

Le compilateur Rust a un vérificateur d'emprunt qui compare les portées pour déterminer si tous les emprunts sont valides. La Liste 10-17 montre le même code que la Liste 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}");   //          |
}                         // ---------+

Liste 10-17: Annotations des durées de vie de r et x, nommées 'a et 'b respectivement

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. À la compilation, Rust compare la taille des deux durées de vie et constate que r a une durée de vie de 'a mais qu'il fait référence à une mémoire avec une durée de vie de 'b. Le programme est rejeté car 'b est plus courte que 'a : l'objet de la référence ne vit pas aussi longtemps que la référence.

La Liste 10-18 corrige le code pour qu'il n'ait pas de référence fausse et qu'il compile sans erreur.

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {r}");   //   |       |
                          // --+       |
}                         // ----------+

Liste 10-18: Une référence valide car les données ont une durée de vie plus longue que la référence

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

Maintenant que vous savez où sont les durées de vie des références et comment Rust analyse les durées de vie pour vous pouvez être certain que les références seront toujours valides, explorons les durées de vie génériques des paramètres et des valeurs de retour dans le contexte des fonctions.

Generic Lifetimes in Functions

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

Nom de fichier : 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}");
}

Liste 10-19: Une fonction main qui appelle la fonction longest pour trouver la plus longue des deux slices de chaîne de caractères

Notez que nous voulons que la fonction prenne des slices de chaîne 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 propriété de ses paramètres. Consultez "String Slices as Parameters" pour plus de discussions sur pourquoi les paramètres que nous utilisons dans la Liste 10-19 sont les paramètres que nous voulons.

Si nous essayons d'implémenter la fonction longest comme indiqué dans la Liste 10-20, elle ne compilera pas.

Nom de fichier : src/main.rs

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Liste 10-20: Une implémentation de la fonction longest qui renvoie la plus longue des deux slices de chaîne de caractères mais qui ne compile pas encore

Au lieu de cela, nous obtenons l'erreur suivante qui parle des durées de vie :

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 {
  |           ++++     ++          ++          ++

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 dire si la référence renvoyée fait référence à x ou à y. En fait, nous ne savons pas non plus, car le bloc if dans le corps de cette fonction renvoie une référence à x et le bloc else renvoie une référence à 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 Listes 10-17 et 10-18 pour déterminer si la référence que nous renvoyons sera toujours valide. Le vérificateur d'emprunt ne peut pas non plus déterminer cela, 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 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 effectuer son analyse.

Lifetime Annotation Syntax

Les annotations de durée de vie ne changent pas la durée de vie de l'une quelconque des références. Au contraire, elles décrivent les relations entre les durées de vie de plusieurs références les unes par rapport aux autres sans affecter les durées de vie. 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 tous 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ètres 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 à 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 nommé 'a, et une référence mutable à un i32 qui a également 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 prise seule n'a pas beaucoup de sens car les annotations sont destinées à dire à 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. Examnons comment les annotations de durée de vie sont liées les unes aux autres dans le contexte de la fonction longest.

Lifetime Annotations in Function Signatures

Pour utiliser des 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 crochets angulaires, entre le nom de la fonction et la liste de paramètres, tout comme nous l'avons fait avec les paramètres de type génériques.

Nous voulons que la signature exprime la contrainte suivante : la référence renvoyée sera valide aussi longtemps que les deux paramètres le seront. C'est la relation entre les durées de vie des paramètres et de la valeur de retour. Nous nommerons la durée de vie 'a puis l'ajouterons à chaque référence, comme indiqué dans la Liste 10-21.

Nom de fichier : src/main.rs

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

Liste 10-21 : La définition de la fonction longest spécifiant que toutes les références dans la signature doivent avoir la même durée de vie 'a

Ce code devrait compiler et produire le résultat que nous voulons lorsqu'on l'utilise avec la fonction main de la Liste 10-19.

La signature de la fonction indique désormais à Rust qu'au cours d'une certaine durée de vie 'a, la fonction prend deux paramètres, qui sont tous deux des slices de chaîne de caractères qui existent au moins 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 renvoyée par la fonction existera 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 renvoyée par la fonction longest est la même que la plus courte des durées de vie des valeurs référencées par les arguments de la fonction. Ces relations sont celles 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 de toutes les valeurs passées en paramètre ou renvoyées. Au contraire, nous spécifions que le vérificateur d'emprunt devrait rejeter toutes les valeurs qui ne respectent pas ces contraintes. Notez que la fonction longest n'a pas besoin de savoir exactement combien de temps x et y existeront, seulement qu'un certain contexte peut être substitué à 'a qui satisfait cette signature.

Lorsque l'on annote les durées de vie dans les fonctions, les annotations se trouvent dans la signature de la fonction, pas dans le corps de la fonction. Les annotations de durée de vie deviennent partie du contrat de la fonction, tout comme les types dans la signature. Le fait que les signatures de fonctions contiennent le contrat de durée de vie signifie que l'analyse effectuée par le compilateur Rust peut être plus simple. Si un problème se produit dans la manière dont une fonction est annotée ou appelée, les erreurs du compilateur peuvent pointer plus précisément vers la partie de notre code et les contraintes. Si, au lieu de cela, le compilateur Rust faisait plus d'inférences sur ce que nous pensions être les relations entre les durées de vie, le compilateur pourrait seulement être en mesure de pointer 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 du contexte de x qui chevauche le contexte 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 courte des durées de vie de x et y. Parce que nous avons annoté la référence renvoyée avec le même paramètre de durée de vie 'a, la référence renvoyée sera également valide pour la durée de la plus courte des durées de vie de x et y.

Regardons 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. La Liste 10-22 est un exemple simple.

Nom de fichier : 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}");
    }
}

Liste 10-22 : Utilisation de la fonction longest avec des références à des valeurs de type String qui ont des durées de vie concrètes différentes

Dans cet exemple, string1 est valide jusqu'à la fin du contexte externe, string2 est valide jusqu'à la fin du contexte interne, et result référence quelque chose qui est valide jusqu'à la fin du contexte interne. Exécutez ce code et vous verrez que le vérificateur d'emprunt l'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 courte des deux arguments. Nous allons déplacer la déclaration de la variable result en dehors du contexte interne, mais laisser l'affectation de la valeur à la variable result à l'intérieur du contexte avec string2. Ensuite, nous déplacerons l'instruction println! qui utilise result en dehors du contexte interne, après que le contexte interne soit terminé. Le code de la Liste 10-23 ne compilera pas.

Nom de fichier : 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}");
}

Liste 10-23 : Tentative d'utilisation de result après que string2 est sortie de portée

Lorsque nous essayons de compiler ce code, nous obtenons cette erreur :

error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
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

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

En tant qu'humains, nous pouvons examiner ce code et voir que string1 est plus longue que string2, et donc, result contiendra une référence à string1. Parce que string1 n'est pas sortie de portée encore, une référence à 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 renvoyée par la fonction longest est la même que la plus courte des durées de vie des références passées en paramètre. Par conséquent, le vérificateur d'emprunt interdit le code de la Liste 10-23 comme pouvant potentiellement avoir une référence invalide.

Essayez de concevoir d'autres expériences qui varient les valeurs et les durées de vie des références passées à la fonction longest et la manière dont la référence renvoyée est utilisée. Faites des hypothèses sur le fait que vos expériences passeront le vérificateur d'emprunt avant de compiler ; puis vérifiez si vous avez raison!

Thinking in Terms of Lifetimes

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

Nom de fichier : src/main.rs

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'a aucune relation avec la durée de vie de x ou de la valeur de retour.

Lorsque l'on renvoie une référence à partir d'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 d'un des paramètres. Si la référence renvoyée ne Fait PAS référence à l'un des paramètres, elle doit faire référence à une valeur créée à l'intérieur de cette fonction. Cependant, ce serait une référence fausse car la valeur sortira de portée à la fin de la fonction. Considérez cette tentative d'implémentation de la fonction longest qui ne compilera pas :

Nom de fichier : src/main.rs

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 :

error[E0515]: cannot return reference to local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ^^^^^^^^^^^^^^^ returns a reference to data owned by the
current function

Le problème est que result sort de portée et est nettoyé à la fin de la fonction longest. Nous essayons également de renvoyer une référence à result à partir de la fonction. Il n'y a aucun moyen pour nous de spécifier des paramètres de durée de vie qui changeraient la référence fausse, et Rust ne nous laissera pas créer une référence fausse. Dans ce cas, la meilleure solution serait de renvoyer un type de données propriétaire plutôt qu'une référence, de sorte que la fonction appelante soit alors responsable de la nettoyage de la valeur.

En fin de compte, la syntaxe de durée de vie est destinée à connecter les durées de vie de divers paramètres et de valeurs de retour de fonctions. Une fois qu'elles sont connectées, Rust dispose d'informations suffisantes pour autoriser des opérations sécurisées en mémoire et interdire les opérations qui créeraient des pointeurs faussement rattachés ou qui enfreindraient autrement la sécurité mémoire.

Lifetime Annotations in Struct Definitions

Jusqu'à présent, les structs que nous avons définis contiennent tous des types propriétaires. Nous pouvons définir des structs pour contenir des références, mais dans ce cas, nous devons ajouter une annotation de durée de vie à chaque référence dans la définition du struct. La Liste 10-24 présente un struct nommé ImportantExcerpt qui contient une slice de chaîne de caractères.

Nom de fichier : src/main.rs

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

fn main() {
  3 let novel = String::from(
        "Call me Ishmael. Some years ago..."
    );
  4 let first_sentence = novel
       .split('.')
       .next()
       .expect("Could not find a '.'");
  5 let i = ImportantExcerpt {
        part: first_sentence,
    };
}

Liste 10-24: Un struct qui contient une référence, nécessitant une annotation de durée de vie

Ce struct a le seul champ part qui contient une slice de chaîne de caractères, qui est une référence [2]. 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 crochets angulaires après le nom du struct afin que nous puissions utiliser le paramètre de durée de vie dans le corps de la définition du struct [1]. Cette annotation signifie qu'une instance de ImportantExcerpt ne peut pas avoir une durée de vie supérieure à celle de la référence qu'elle contient dans son champ part.

La fonction main ici crée une instance du struct ImportantExcerpt [5] qui contient une référence à la première phrase de la String [4] propriété de la variable novel [3]. Les données dans novel existent avant la création de l'instance ImportantExcerpt. De plus, novel ne sort pas de portée avant que l'instance ImportantExcerpt ne sorte de portée, donc la référence dans l'instance ImportantExcerpt est valide.

Lifetime Elision

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

Nom de fichier : 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[..]
}

Liste 10-25: Une fonction que nous avons définie dans la Liste 4-9 qui a compilé sans annotations de durée de vie, même si le type de paramètre et de retour est une référence

La raison pour laquelle cette fonction compile sans annotations de durée de vie est historique : dans les premières versions (avant 1.0) de Rust, ce code n'aurait pas compilé car chaque référence avait besoin d'une durée de vie explicite. À l'é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 entrait les mêmes annotations de durée de vie à répétition 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 de sorte que le vérificateur d'emprunt puisse inférer les durées de vie dans ces situations et n'ait pas besoin d'annotations explicites.

Cette partie de l'histoire de Rust est pertinente car il est possible que de nouveaux modèles déterministes apparaissent et soient ajoutés au compilateur. Dans l'avenir, peut-être que même moins d'annotations de durée de vie seront requises.

Les modèles programmés dans l'analyse de références de Rust sont appelés les règles d'élision de durée de vie. Ce ne sont pas des règles pour les programmeurs à 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 explicitement les durées de vie.

Les règles d'élision ne fournissent pas une inférence complète. Si Rust applique de manière déterministe les règles mais qu'il reste une ambiguïté quant aux durées de vie des références, 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 encore 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 de fn ainsi qu'aux blocs impl.

La première règle est que le compilateur attribue 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 attribué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 a plusieurs paramètres de durée de vie d'entrée, mais que l'un d'entre eux est &self ou &mut self car il s'agit d'une méthode, la durée de vie de self est attribué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 il est nécessaire de moins de symboles.

Pretendons que nous soyons le compilateur. Nous allons appliquer ces règles pour déterminer les durées de vie des références dans la signature de la fonction first_word de la Liste 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 la suivante :

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

La deuxième règle s'applique car il y a exactement un paramètre de durée de vie d'entrée. La deuxième règle spécifie que la durée de vie du paramètre d'entrée unique est attribuée à la durée de vie de sortie, donc la signature est maintenant la suivante :

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 que le programmeur ait besoin d'annoter 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 l'avons commencé à utiliser dans la Liste 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'un paramètre de 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 parcouru 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 eu une erreur en essayant de compiler le code de la Liste 10-20 : le compilateur a parcouru les règles d'élision de durée de vie mais n'a toujours pas pu déterminer toutes les durées de vie des références dans la signature.

Parce que la troisième règle ne s'applique vraiment que dans les signatures de méthodes, nous allons examiner les durées de vie dans ce contexte ensuite pour voir pourquoi la troisième règle signifie que nous n'avons pas besoin d'annoter les durées de vie dans les signatures de méthodes très souvent.

Lifetime Annotations in Method Definitions

Lorsque nous implémentons des méthodes sur un struct avec des durées de vie, nous utilisons la même syntaxe que celle des paramètres de type générique montrée dans la Liste 10-11. Où nous déclarons et utilisons les paramètres de durée de vie dépend de leur relation avec les champs du struct ou les paramètres et valeurs de retour de la méthode.

Les noms de durées de vie pour les champs du struct doivent toujours être déclarés après le mot clé impl puis utilisés après le nom du struct car ces durées de vie font partie du type du 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 du struct, ou elles peuvent être indépendantes. De plus, les règles d'élision de durée de vie font souvent en sorte qu'aucune annotation de durée de vie n'est nécessaire dans les signatures de méthodes. Regardons quelques exemples en utilisant le struct nommé ImportantExcerpt que nous avons défini dans la Liste 10-24.

Tout d'abord, nous utiliserons une méthode nommée level dont le seul paramètre est une référence à self et dont la valeur de retour est un i32, qui n'est pas une référence à quoi que ce soit :

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

La déclaration du paramètre de durée de vie après impl et son utilisation après le nom du type est requise, mais nous n'avons pas besoin d'annoter la durée de vie de la référence à self en raison de la première règle d'élision.

Voici un exemple où la troisième règle d'élision de durée de vie s'applique :

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

Il y a deux durées de vie d'entrée, donc Rust applique la première règle d'élision de durée de vie et attribue à &self et announcement leurs propres durées de vie. Ensuite, parce que 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.

The Static Lifetime

Une durée de vie spéciale que nous devons discuter est 'static, qui indique que la référence affectée peut exister pendant toute la durée du programme. Tous les littéraux de chaîne de caractères ont la durée de vie 'static, que nous pouvons annoter comme suit :

let s: &'static str = "I have a static lifetime.";

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

Vous pouvez voir des suggestions d'utiliser la durée de vie 'static dans les messages d'erreur. Mais avant de spécifier 'static comme durée de vie pour une référence, réfléchissez à savoir si la référence que vous avez réellement existe pendant toute la durée de votre programme ou non, et si vous le voulez. Dans la plupart des cas, un message d'erreur suggérant la durée de vie 'static résulte d'une tentative de créer une référence fausse ou d'un chevauchement des durées de vie disponibles. Dans de tels cas, la solution est de corriger ces problèmes, et non pas de spécifier la durée de vie 'static.

Generic Type Parameters, Trait Bounds, and Lifetimes Together

Jetons un coup d'œil rapide à la syntaxe de spécification des paramètres de type générique, des contraintes de trait et des durées de vie dans une seule fonction!

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
    }
}

Il s'agit de la fonction longest de la Liste 10-21 qui renvoie la chaîne de caractères la plus longue parmi deux slices de chaîne. Mais maintenant, elle a un paramètre supplémentaire nommé ann du type générique T, qui peut être remplacé par n'importe quel type qui implémente le trait Display tel que spécifié dans la clause where. Ce paramètre supplémentaire sera affiché en utilisant {}, ce qui explique pourquoi la contrainte de trait Display est nécessaire. Étant donné que les durées de vie sont un type de paramètre 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 se trouvent dans la même liste à l'intérieur des crochets angulaires après le nom de la fonction.

Summary

Félicitations! Vous avez terminé le laboratoire Validating References With Lifetimes. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.