Eu estava lendo o capítulo de duração do livro Rust e me deparei com este exemplo por uma vida nomeada / explícita:
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let x; // -+ x goes into scope
// |
{ // |
let y = &5; // ---+ y goes into scope
let f = Foo { x: y }; // ---+ f goes into scope
x = &f.x; // | | error here
} // ---+ f and y go out of scope
// |
println!("{}", x); // |
} // -+ x goes out of scope
Está bem claro para mim que o erro que está sendo evitado pelo compilador é o uso após a liberação da referência atribuída a x
: depois que o escopo interno é concluído f
e , portanto, &f.x
torna - se inválido e não deveria ter sido atribuído x
.
Meu problema é que o problema poderia ser facilmente analisado sem o tempo de vida explícito 'a
, por exemplo, deduzindo uma atribuição ilegal de uma referência a um escopo mais amplo ( x = &f.x;
).
Em quais casos as vidas explícitas são realmente necessárias para evitar erros de uso após liberação (ou de alguma outra classe?)?
reference
rust
static-analysis
lifetime
corazza
fonte
fonte
Respostas:
As outras respostas têm todos pontos importantes ( o exemplo concreto de fjh, onde é necessária uma vida explícita ), mas faltam uma coisa importante: por que são necessárias vidas explícitas quando o compilador diz que você as entendeu errado ?
Esta é realmente a mesma pergunta que "por que tipos explícitos são necessários quando o compilador pode inferir". Um exemplo hipotético:
Obviamente, o compilador pode ver que estou retornando um
&'static str
, então por que o programador precisa digitá-lo?O principal motivo é que, embora o compilador possa ver o que seu código faz, ele não sabe qual era sua intenção.
As funções são um limite natural para proteger os efeitos da alteração de código. Se permitirmos que as vidas úteis sejam completamente inspecionadas a partir do código, uma mudança de aparência inocente pode afetar as vidas úteis, o que poderia causar erros em uma função distante. Este não é um exemplo hipotético. Pelo que entendi, Haskell tem esse problema quando você confia na inferência de tipo para funções de nível superior. Rust cortou esse problema em particular pela raiz.
Há também um benefício de eficiência para o compilador - apenas as assinaturas de função precisam ser analisadas para verificar tipos e vida útil. Mais importante, ele tem um benefício de eficiência para o programador. Se não tivemos vidas explícitas, o que essa função faz:
É impossível saber sem inspecionar a fonte, o que contraria um grande número de práticas recomendadas de codificação.
Escopos são vidas, essencialmente. Um pouco mais claramente, a vida útil
'a
é um parâmetro genérico de vida útil que pode ser especializado com um escopo específico em tempo de compilação, com base no site de chamada.De modo nenhum. É necessário um tempo de vida para evitar erros, mas um tempo de vida explícito é necessário para proteger o pouco que os programadores de sanidade têm.
fonte
f x = x + 1
sem uma assinatura de tipo que você está usando em outro módulo. Se você alterar posteriormente a definição paraf x = sqrt $ x + 1
, seu tipo mudará deNum a => a -> a
paraFloating a => a -> a
, o que causará erros de tipo em todos os locais de chamada ondef
é chamado, por exemplo, com umInt
argumento. Ter uma assinatura de tipo garante que os erros ocorram localmente.sqrt $
, apenas um erro local teria ocorrido após a alteração e não muitos erros em outros lugares (o que é muito melhor se não deseja alterar o tipo real)?Vamos dar uma olhada no exemplo a seguir.
Aqui, as vidas explícitas são importantes. Isso é compilado porque o resultado de
foo
tem a mesma vida útil do primeiro argumento ('a
), portanto, pode sobreviver ao segundo argumento. Isso é expresso pelos nomes vitalícios na assinatura defoo
. Se você alternasse os argumentos na chamada parafoo
o compilador reclamaria quey
não dura o suficiente:fonte
A anotação de duração na seguinte estrutura:
especifica que uma
Foo
instância não deve sobreviver à referência que ela contém (x
campo).O exemplo que você encontrou no livro Rust não ilustra isso porque
f
ey
variáveis ficam fora do escopo ao mesmo tempo.Um exemplo melhor seria o seguinte:
Agora,
f
realmente sobrevive à variável apontada porf.x
.fonte
Observe que não há vida útil explícita nesse trecho de código, exceto a definição da estrutura. O compilador é perfeitamente capaz de inferir vidas úteis
main()
.Nas definições de tipo, no entanto, as vidas explícitas são inevitáveis. Por exemplo, há uma ambiguidade aqui:
Devem ter vidas diferentes ou devem ser iguais? Importa da perspectiva do uso,
struct RefPair<'a, 'b>(&'a u32, &'b u32)
é muito diferentestruct RefPair<'a>(&'a u32, &'a u32)
.Agora, para casos simples, como o que você forneceu, o compilador poderia teoricamente eliminar vidas como em outros lugares, mas esses casos são muito limitados e não valem complexidade extra no compilador, e esse ganho em clareza estaria no muito menos questionável.
fonte
'static
,'static
podem ser usadas em todos os lugares onde vidas locais podem ser usadas; portanto, no seu exemplop
, seu parâmetro de vida útil será inferido como a vida útil localy
.RefPair<'a>(&'a u32, &'a u32)
significa que'a
será a interseção das duas vidas úteis de entrada, ou seja, neste caso, a vida útil dey
.O caso do livro é muito simples por design. O tópico das vidas úteis é considerado complexo.
O compilador não pode inferir facilmente a vida útil de uma função com vários argumentos.
Além disso, minha própria caixa opcional possui um
OptionBool
tipo com umas_slice
método cuja assinatura é realmente:Não há absolutamente nenhuma maneira de o compilador ter descoberto isso.
fonte
Encontrei outra ótima explicação aqui: http://doc.rust-lang.org/0.12.0/guide-lifetimes.html#returning-references .
fonte
Se uma função recebe duas referências como argumentos e retorna uma referência, a implementação da função às vezes pode retornar a primeira referência e outras a segunda. É impossível prever qual referência será retornada para uma determinada chamada. Nesse caso, é impossível inferir uma vida útil para a referência retornada, pois cada referência de argumento pode se referir a uma ligação de variável diferente com uma vida útil diferente. As vidas explícitas ajudam a evitar ou esclarecer tal situação.
Da mesma forma, se uma estrutura contém duas referências (como dois campos de membro), uma função de membro da estrutura pode às vezes retornar a primeira referência e outras vezes a segunda. Mais uma vez, vidas explícitas impedem essas ambiguidades.
Em algumas situações simples, há elisão da vida em que o compilador pode inferir vidas.
fonte
A razão pela qual seu exemplo não funciona é simplesmente porque o Rust possui apenas vida útil local e inferência de tipo. O que você está sugerindo exige inferência global. Sempre que você tiver uma referência cujo tempo de vida não possa ser eliminado, ela deverá ser anotada.
fonte
Como recém-chegado ao Rust, meu entendimento é que vidas explícitas servem a dois propósitos.
Colocar uma anotação de duração explícita em uma função restringe o tipo de código que pode aparecer dentro dessa função. A vida útil explícita permite ao compilador garantir que seu programa esteja fazendo o que você pretendia.
Se você (o compilador) deseja verificar se um trecho de código é válido, você (o compilador) não precisará procurar iterativamente todas as funções chamadas. Basta dar uma olhada nas anotações de funções chamadas diretamente por esse trecho de código. Isso torna seu programa muito mais fácil de raciocinar para você (o compilador) e torna os tempos de compilação gerenciáveis.
No ponto 1., considere o seguinte programa escrito em Python:
que imprimirá
Esse tipo de comportamento sempre me surpreende. O que está acontecendo é que o
df
compartilhamento de memória é feitoar
; portanto, quando parte do conteúdo dasdf
alterações éwork
afetadaar
também. No entanto, em alguns casos, isso pode ser exatamente o que você deseja, por motivos de eficiência de memória (sem cópia). O verdadeiro problema nesse código é que a funçãosecond_row
está retornando a primeira linha em vez da segunda; boa sorte depurando isso.Considere um programa semelhante escrito em Rust:
Compilando isso, você obtém
Na verdade, você recebe dois erros, há também um com os papéis de
'a
e'b
intercambiado. Observando a anotação desecond_row
, descobrimos que a saída deve ser&mut &'b mut [i32]
, ou seja, a saída deve ser uma referência a uma referência com vida útil'b
(a vida útil da segunda linha deArray
). No entanto, como estamos retornando a primeira linha (que possui vida útil'a
), o compilador reclama da incompatibilidade da vida útil. No lugar certo. No tempo certo. Depurar é fácil.fonte
Penso em uma anotação vitalícia como um contrato sobre uma determinada referência válida no escopo de recebimento apenas enquanto permanecer válida no escopo de origem. Declarar mais referências no mesmo período de vida mescla os escopos, o que significa que todas as referências de origem precisam satisfazer esse contrato. Essa anotação permite que o compilador verifique o cumprimento do contrato.
fonte