É mais fácil entender o que são vidas não lexicais entendendo o que são vidas lexicais . Em versões do Rust antes que existam tempos de vida não lexicais, este código falhará:
fn main() {
let mut scores = vec![1, 2, 3];
let score = &scores[0];
scores.push(4);
}
O compilador Rust vê que scores
foi emprestado pela score
variável, portanto, não permite mais mutações 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
No entanto, um humano pode ver trivialmente que este exemplo é excessivamente conservador: nuncascore
é usado ! O problema é que o empréstimo de scores
by score
é léxico - dura até o final do bloco em que está inserido:
fn main() {
let mut scores = vec![1, 2, 3];
let score = &scores[0];
scores.push(4);
}
Os tempos de vida não lexicais corrigem isso aprimorando o compilador para entender esse nível de detalhe. O compilador agora pode dizer com mais precisão quando um empréstimo é necessário e esse código será compilado.
Uma coisa maravilhosa sobre vidas não lexicais é que, uma vez ativadas, ninguém jamais pensará nelas . Isso simplesmente se tornará "o que Rust faz" e as coisas (com sorte) simplesmente funcionarão.
Por que existências lexicais foram permitidas?
O Rust destina-se a permitir apenas a compilação de programas conhecidos e seguros. No entanto, é impossível permitir exatamente apenas programas seguros e rejeitar os não seguros. Nesse sentido, Rust erra por ser conservador: alguns programas seguros são rejeitados. As vidas lexicais são um exemplo disso.
Os tempos de vida lexicais eram muito mais fáceis de implementar no compilador porque o conhecimento dos blocos é "trivial", enquanto o conhecimento do fluxo de dados é menos. O compilador precisava ser reescrito para introduzir e fazer uso de uma "representação intermediária de nível médio" (MIR) . Em seguida, o verificador de empréstimo (também conhecido como "empréstimo") teve que ser reescrito para usar MIR em vez da árvore de sintaxe abstrata (AST). Então, as regras do verificador de empréstimo tiveram que ser refinadas para serem mais refinadas.
As vidas lexicais nem sempre atrapalham o programador, e há muitas maneiras de contornar as vidas lexicais quando o fazem, mesmo que sejam irritantes. Em muitos casos, isso envolvia a adição de colchetes extras ou um valor booleano. Isso permitiu que o Rust 1.0 fosse enviado e fosse útil por muitos anos antes da implementação de vidas não lexicais.
Curiosamente, certos bons padrões foram desenvolvidos por causa de vidas lexicais. O principal exemplo para mim é o entry
padrão . Este código falha antes de tempos de vida não lexicais e é compilado com ele:
fn example(mut map: HashMap<i32, i32>, key: i32) {
match map.get_mut(&key) {
Some(value) => *value += 1,
None => {
map.insert(key, 1);
}
}
}
No entanto, esse código é ineficiente porque calcula o hash da chave duas vezes. A solução que foi criada por causa de tempos de vida lexicais é mais curta e mais eficiente:
fn example(mut map: HashMap<i32, i32>, key: i32) {
*map.entry(key).or_insert(0) += 1;
}
O nome "vidas não lexicais" não parece certo para mim
O tempo de vida de um valor é o intervalo de tempo durante o qual o valor permanece em um endereço de memória específico (consulte Por que não posso armazenar um valor e uma referência a esse valor na mesma estrutura? Para obter uma explicação mais longa). O recurso conhecido como vidas não lexicais não altera as vidas de nenhum valor, portanto, não pode tornar as vidas não lexicais. Isso apenas torna o rastreamento e a verificação dos empréstimos desses valores mais precisos.
Um nome mais preciso para o recurso pode ser " empréstimos não lexicais ". Alguns desenvolvedores de compiladores referem-se ao "empréstimo baseado em MIR" subjacente.
Os tempos de vida não lexicais nunca tiveram a intenção de ser um recurso "voltado para o usuário", por si só . Eles cresceram principalmente em nossas mentes por causa dos pequenos recortes de papel que obtemos de sua ausência. O nome deles era destinado principalmente para fins de desenvolvimento interno e alterá-lo para fins de marketing nunca foi uma prioridade.
Sim, mas como faço para usá-lo?
No Rust 1.31 (lançado em 06-12-2018), você precisa ativar a edição Rust 2018 em seu Cargo.toml:
[package]
name = "foo"
version = "0.0.1"
authors = ["An Devloper <an.devloper@example.com>"]
edition = "2018"
A partir do Rust 1.36, a edição Rust 2015 também permite vidas não lexicais.
A implementação atual de tempos de vida não lexicais está em um "modo de migração". Se o verificador de empréstimo do NLL for aprovado, a compilação continuará. Caso contrário, o verificador de empréstimo anterior é invocado. Se o antigo verificador de empréstimo permitir o código, um aviso é impresso, informando que seu código provavelmente quebrará em uma versão futura do Rust e deve ser atualizado.
Nas versões noturnas do Rust, você pode optar pela quebra forçada por meio de um sinalizador de recurso:
#![feature(nll)]
Você pode até optar pela versão experimental do NLL usando o sinalizador do compilador -Z polonius
.
Uma amostra de problemas reais resolvidos por vidas não lexicais