Il est plus facile de comprendre ce que sont les durées de vie non lexicales en comprenant ce que sont les durées de vie lexicales . Dans les versions de Rust avant que les durées de vie non lexicales soient présentes, ce code échouera:
fn main() {
let mut scores = vec![1, 2, 3];
let score = &scores[0];
scores.push(4);
}
Le compilateur Rust voit que scores
c'est emprunté par la score
variable, donc il interdit toute mutation ultérieure de scores
:
error[E0502]: cannot borrow `scores` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let score = &scores[0];
| ------ immutable borrow occurs here
4 | scores.push(4);
| ^^^^^^ mutable borrow occurs here
5 | }
| - immutable borrow ends here
Cependant, un humain peut voir trivialement que cet exemple est trop conservateur: score
n'est jamais utilisé ! Le problème est que l'emprunt de scores
by score
est lexical - il dure jusqu'à la fin du bloc dans lequel il est contenu:
fn main() {
let mut scores = vec![1, 2, 3];
let score = &scores[0];
scores.push(4);
}
Les durées de vie non lexicales corrigent ce problème en améliorant le compilateur pour comprendre ce niveau de détail. Le compilateur peut maintenant dire avec plus de précision quand un emprunt est nécessaire et ce code se compilera.
Une chose merveilleuse à propos des vies non lexicales est qu'une fois activées, personne ne pensera jamais à elles . Cela deviendra simplement "ce que Rust fait" et les choses fonctionneront (espérons-le).
Pourquoi les vies lexicales ont-elles été autorisées?
Rust est conçu pour permettre uniquement la compilation de programmes connus de sécurité. Cependant, il est impossible d'autoriser exactement uniquement les programmes sûrs et de rejeter ceux qui ne le sont pas. À cette fin, Rust se trompe sur le côté conservateur: certains programmes sûrs sont rejetés. Les durées de vie lexicales en sont un exemple.
Les durées de vie lexicales étaient beaucoup plus faciles à implémenter dans le compilateur car la connaissance des blocs est "triviale", alors que la connaissance du flux de données l'est moins. Le compilateur avait besoin d'être réécrit pour introduire et utiliser une "représentation intermédiaire de niveau intermédiaire" (MIR) . Ensuite, le vérificateur d'emprunt (alias "borrowck") a dû être réécrit pour utiliser MIR au lieu de l'arbre de syntaxe abstraite (AST). Ensuite, les règles du vérificateur d'emprunt ont dû être affinées pour être plus fines.
Les durées de vie lexicales ne gênent pas toujours le programmeur, et il existe de nombreuses façons de contourner les durées de vie lexicales lorsqu'elles le font, même si elles sont ennuyeuses. Dans de nombreux cas, cela impliquait l'ajout d'accolades supplémentaires ou d'une valeur booléenne. Cela a permis à Rust 1.0 d'être livré et d'être utile pendant de nombreuses années avant que des vies non lexicales ne soient implémentées.
Fait intéressant, certains bons modèles ont été développés en raison de la durée de vie lexicale. Le meilleur exemple pour moi est le entry
modèle . Ce code échoue avant des vies non lexicales et se compile avec:
fn example(mut map: HashMap<i32, i32>, key: i32) {
match map.get_mut(&key) {
Some(value) => *value += 1,
None => {
map.insert(key, 1);
}
}
}
Cependant, ce code est inefficace car il calcule deux fois le hachage de la clé. La solution créée en raison des durées de vie lexicales est plus courte et plus efficace:
fn example(mut map: HashMap<i32, i32>, key: i32) {
*map.entry(key).or_insert(0) += 1;
}
Le nom "vies non lexicales" ne me semble pas correct
La durée de vie d'une valeur est l'intervalle de temps pendant lequel la valeur reste à une adresse mémoire spécifique (voir Pourquoi ne puis-je pas stocker une valeur et une référence à cette valeur dans la même structure? Pour une explication plus longue). La fonctionnalité connue sous le nom de durées de vie non lexicales ne modifie la durée de vie d'aucune valeur, elle ne peut donc pas rendre les durées de vie non lexicales. Il ne fait que rendre le suivi et la vérification des emprunts de ces valeurs plus précis.
Un nom plus précis pour la fonction pourrait être « emprunts non lexicaux ». Certains développeurs de compilateurs se réfèrent au "borrowck basé sur MIR" sous-jacent.
Les durées de vie non lexicales n'ont jamais été destinées à être une fonction "face à l'utilisateur" en soi . Ils ont surtout grandi dans nos esprits à cause des petites coupures de papier que nous obtenons de leur absence. Leur nom était principalement destiné à des fins de développement interne et le changer à des fins de marketing n'a jamais été une priorité.
Ouais, mais comment l'utiliser?
Dans Rust 1.31 (publié le 06/12/2018), vous devez activer l'édition Rust 2018 dans votre Cargo.toml:
[package]
name = "foo"
version = "0.0.1"
authors = ["An Devloper <an.devloper@example.com>"]
edition = "2018"
Depuis Rust 1.36, l'édition Rust 2015 permet également des durées de vie non lexicales.
L'implémentation actuelle des durées de vie non lexicales est en "mode de migration". Si le vérificateur d'emprunt NLL réussit, la compilation se poursuit. Si ce n'est pas le cas, le vérificateur d'emprunt précédent est appelé. Si l'ancien vérificateur d'emprunt autorise le code, un avertissement est imprimé, vous informant que votre code est susceptible de se casser dans une future version de Rust et doit être mis à jour.
Dans les versions nocturnes de Rust, vous pouvez activer la rupture forcée via un indicateur de fonctionnalité:
#![feature(nll)]
Vous pouvez même opter pour la version expérimentale de NLL en utilisant l'indicateur du compilateur -Z polonius
.
Un échantillon de problèmes réels résolus par des vies non lexicales