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

Encart 10-17 : tentative d'utiliser une référence vers une valeur qui est sortie de la portée

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); //          |
    }                         // ---------+
}

Encart 10-18 : commentaires pour montrer les durées de vie de r et x, qui s'appellent respectivement 'a et 'b

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); //   |       |
                              // --+       |
    }                         // ----------+
}

Encart 10-19 : la référence est valide puisque la donnée a une durée de vie plus longue que la référence

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

Encart 10-20 : une fonction main qui appelle la fonction la_plus_longue pour trouver la plus grande des deux slices de chaîne de caractères

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

Encart 10-21 : une implémentation de la fonction la_plus_longue qui retourne la plus longue des deux slices de chaînes de caractères, mais ne se compile pas encore

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

Encart 10-22 : définition de la fonction la_plus_longue qui indique que toutes les références présentes dans la signature doivent avoir la même durée de vie 'a

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

Encart 10-23 : utilisation de la fonction la_plus_longue sur des références à des valeurs String qui ont concrètement des durées de vie différentes

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

Encart 10-24 : tentative d'utilisation de resultat après string2, qui est sortie de la portée

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

Encart 10-25 : une structure qui stocke une référence, par conséquent sa définition a besoin d'une annotation de durée de vie

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

Encart 10-26 : une fonction que nous avons défini dans l'encart 4-9 qui se compilait sans avoir d'indications sur la durée de vie, même si les paramètres et le type de retour sont des références

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.