Pourquoi Rust oblige-t-il à spécifier les durées de vie ? Et comment cela simplifie-t-il la vie ?
\n\nSi vous apprenez Rust, vous rencontrerez tôt ou tard cette syntaxe mystérieuse : 'a, 'b... Et une question légitime se pose : « Pourquoi dois-je spécifier explicitement les durées de vie ? D'autres langages s'en passent ! »
Cette question est l'une des plus populaires sur Stack Overflow (des milliers de vues). Et ce n'est pas surprenant : le concept des lifetimes en Rust semble complexe jusqu'à ce que l'on en comprenne l'essence. Démêlons cela sans théorie ennuyeuse, avec des exemples concrets.
\n\nRéponse courte : la sécurité sans ramasse-miettes
\n\nRust garantit la sécurité mémoire à la compilation. Il a besoin de savoir exactement combien de temps vivent les références pour éviter les références pendantes (dangling references). En Java ou Go, c'est le ramasse-miettes qui s'en charge. En C++, vous êtes responsable de la mémoire (et vous faites souvent des erreurs). Rust a choisi une troisième voie : le compilateur vérifie tout statiquement.
\n\nMais pourquoi le compilateur ne peut-il pas le deviner tout seul ? Il le peut – dans 80 % des cas. Cela s'appelle lifetime elision (règles d'inférence implicite). Cependant, dans des situations complexes, il a besoin de votre aide.
\n\nRègle n°1 : Le compilateur n'est pas un médium
\n\nRegardez cette fonction :
\n\nfn longest(x: &str, y: &str) -> &str {\n if x.len() > y.len() { x } else { y }\n}\n\nÀ première vue, tout semble logique. Mais Rust générera une erreur : « missing lifetime specifier ». Pourquoi ?
\n\nImaginez que vous passiez x – une référence vers une chaîne qui vit 5 secondes, et y – une référence vers une chaîne qui vit 1 seconde. La fonction peut retourner l'une ou l'autre. Quelle sera la durée de vie de la référence retournée ? Le compilateur ne le sait pas !
La solution – dire explicitement : « la référence retournée vivra aussi longtemps que les deux références d'entrée » :
\n\nfn longest<'a>(x: &'a str, y: &'a str) -> &'a str {\n if x.len() > y.len() { x } else { y }\n}\n\nLe symbole 'a est une annotation de durée de vie. Nous disons : « les trois références doivent avoir la même durée de vie minimale ». Désormais, le compilateur peut vérifier que le code appelant ne détruit pas les données prématurément.
Règle n°2 : Différentes références – différentes durées de vie
\n\nUne erreur fréquente des débutants est de penser que tous les 'a doivent être identiques. En réalité, on peut spécifier différentes durées de vie :
fn example<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {\n x // Ok, on retourne une référence avec la durée de vie 'a\n // y // Erreur ! y vit 'b, mais on a promis 'a\n}\n\nIci, nous disons explicitement : x et y peuvent avoir des durées de vie différentes. Mais nous ne pouvons retourner que la référence dont la durée de vie correspond à celle promise dans le type de retour.
Règle n°3 : Les structures peuvent aussi stocker des références
\n\nLorsque vous créez une structure qui contient des références, les durées de vie en Rust doivent être obligatoirement spécifiées :
\n\nstruct Book<'a> {\n title: &'a str,\n author: &'a str,\n}\n\nfn main() {\n let title = String::from("Rust Book");\n let book;\n {\n let author = String::from("John Doe");\n book = Book { title: &title, author: &author };\n } // author est détruit ici !\n println!("{}", book.title); // Erreur ! book.author est pendante\n}\n\nLe compilateur ne permettra pas de compiler ce code, car book.author référence une chaîne déjà supprimée. Les lifetimes explicites en Rust permettent de détecter ces erreurs à la compilation, et non à l'exécution.
Règle n°4 : Lifetime Elision – quand on peut ne pas écrire
\n\nRust est suffisamment intelligent pour déduire lui-même les durées de vie dans les cas simples. Voici quand on peut ne pas les spécifier :
\n\n- \n
- Une seule référence d'entrée – la durée de vie de la sortie est égale à celle de l'entrée \n