O que são vidas não lexicais?


87

Rust tem um RFC relacionado a tempos de vida não lexicais que foi aprovado para ser implementado na linguagem por um longo tempo. Recentemente , o suporte do Rust a esse recurso melhorou muito e é considerado completo.

Minha pergunta é: o que exatamente é uma vida não lexical?

Respostas:


130

É 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 scoresfoi emprestado pela scorevariá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 scoresby 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);                 //
                                    // <-- score stops borrowing here
}

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 entrypadrã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


11
Acho que valeria a pena enfatizar que, talvez contra a intuição, as vidas não lexicais não são sobre a vida útil das variáveis, mas sobre a vida útil dos empréstimos. Ou, dito de outra forma, Vida Não-Lexical trata de decorrelating as vidas úteis de variáveis ​​daquelas de empréstimos ... a menos que eu esteja errado? (mas não acho que o NLL muda quando um destruidor é executado)
Matthieu M.

2
" Curiosamente, certos bons padrões foram desenvolvidos por causa de vidas lexicais " - suponho, então, que há o risco de que a existência de NLL possa tornar futuros bons padrões muito mais difíceis de identificar.
1919 eggyal

1
@eggyal é certamente uma possibilidade. Projetar dentro de um conjunto de restrições (mesmo que arbitrário!) Pode levar a projetos novos e interessantes. Sem essas restrições, podemos recorrer a nossos conhecimentos e padrões existentes e nunca aprender ou explorar para encontrar algo novo. Dito isso, provavelmente alguém pensaria "ah, o hash está sendo calculado duas vezes, posso consertar isso" e a API seria criada, mas pode ser mais difícil para os usuários encontrarem a API em primeiro lugar. Espero que ferramentas como o clippy ajudem essas pessoas.
Shepmaster
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.