Que sont les durées de vie non lexicales?

87

Rust a une RFC relative aux durées de vie non lexicales qui a été approuvée pour être implémentée dans le langage depuis longtemps. Récemment , la prise en charge de cette fonctionnalité par Rust s'est beaucoup améliorée et est considérée comme complète.

Ma question est: qu'est-ce qu'une vie non lexicale exactement?

Stargateur
la source

Réponses:

130

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 scoresc'est emprunté par la scorevariable, 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: scoren'est jamais utilisé ! Le problème est que l'emprunt de scoresby scoreest 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);                 //
                                    // <-- score stops borrowing here
}

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 entrymodè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 <[email protected]>"]
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

Shepmaster
la source
11
Je pense qu'il serait utile de souligner que, peut-être de manière contre-intuitive, les vies non lexicales ne concernent pas la durée de vie des variables, mais la durée des emprunts. Ou, autrement dit, les durées de vie non lexicales consistent à décorréler la durée de vie des variables de celle des emprunts ... sauf si je me trompe? (mais je ne pense pas que NLL change quand un destructeur est exécuté)
Matthieu M.
2
" Fait intéressant, certains bons modèles ont été développés en raison de la durée de vie lexicale " - Je suppose donc qu'il y a un risque que l'existence de NLL rende les bons modèles futurs encore plus difficiles à identifier?
eggyal
1
@eggyal c'est certainement une possibilité. Concevoir dans un ensemble de contraintes (même arbitraires!) Peut conduire à de nouvelles conceptions intéressantes. Sans ces contraintes, nous pourrions nous rabattre sur nos connaissances et nos modèles existants et ne jamais apprendre ou explorer pour trouver quelque chose de nouveau. Cela étant dit, quelqu'un penserait probablement "oh, le hachage est calculé deux fois, je peux résoudre ce problème" et l'API serait créée, mais il peut être plus difficile pour les utilisateurs de trouver l'API en premier lieu. J'espère que des outils comme clippy aideront ces gens.
Shepmaster