Por que as vidas explícitas são necessárias em Rust?

Eu estava lendo o capítulo de vidas úteis do livro Rust, e me deparei com este exemplo para uma vida útil nomeada / explícita:

struct Foo { 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 depois de livre da referência atribuída a x : depois que o escopo interno é feito, f e portanto &f.x se tornam inválidos, e não deveriam ter sido designados para x .

Meu problema é que o problema poderia ter sido facilmente analisado sem usar o explícito 'a vida toda, por exemplo, inferindo uma atribuição ilegal de uma referência a um escopo mais amplo ( x = &f.x; ).

Em quais casos as vidas úteis são realmente necessárias para evitar erros de uso após a liberação (ou alguma outra class?)?

As outras respostas têm pontos salientes ( o exemplo concreto de fjh onde uma vida explícita é necessária ), mas faltam uma coisa fundamental: por que são necessários tempos de vida explícitos quando o compilador lhe dirá que você os errou ?

Esta é realmente a mesma pergunta que “por que tipos explícitos são necessários quando o compilador pode inferi-los”. Um exemplo hipotético:

 fn foo() -> _ { "" } 

É claro que o compilador pode ver que estou retornando um &'static str , então por que o programador tem que digitá-lo?

A principal razão é que, enquanto o compilador pode 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 o firewall dos efeitos da alteração do código. Se permitíssemos que as vidas inteiras fossem completamente inspecionadas a partir do código, então uma mudança de aparência inocente poderia afetar a vida útil, o que poderia causar erros em uma function distante. Este não é um exemplo hipotético. Pelo que entendi, Haskell tem esse problema quando você confia na inferência de tipos para funções de nível superior. Ferrugem beliscou aquele problema em particular pela raiz.

Há também um benefício de eficiência para o compilador – apenas assinaturas de function precisam ser analisadas para verificar tipos e tempos de vida. Mais importante, tem um benefício de eficiência para o programador. Se não tivermos vidas explícitas, o que essa function faz:

 fn foo(a: &u8, b: &u8) -> &u8 

É impossível dizer sem inspecionar a fonte, o que iria contra um grande número de melhores práticas de codificação.

inferindo uma atribuição ilegal de uma referência a um escopo mais amplo

Escopos são vidas, essencialmente. Um pouco mais claramente, um tempo 'a vida 'a é um parâmetro de vida útil genérico que pode ser especializado com um escopo específico em tempo de compilation, com base no site de chamada.

tempo de vida explícito é realmente necessário para evitar […] erros?

De modo nenhum. Tempos de vida são necessários para evitar erros, mas são necessários tempos de vida explícitos para proteger os pequenos programadores de sanidade.

Vamos dar uma olhada no exemplo a seguir.

 fn foo< 'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 { x } fn main() { let x = 12; let z: &u32 = { let y = 42; foo(&x, &y) }; } 

Aqui, as vidas explícitas são importantes. Isso é compilado porque o resultado de foo tem o mesmo tempo de vida de seu primeiro argumento ( 'a ), de modo que ele pode sobreviver ao seu segundo argumento. Isso é expresso pelos nomes da vida na assinatura de foo . Se você trocasse os argumentos na chamada para foo o compilador reclamaria que y não vive o suficiente:

 error[E0597]: `y` does not live long enough --> src/main.rs:10:5 | 9 | foo(&y, &x) | - borrow occurs here 10 | }; | ^ `y` dropped here while still borrowed 11 | } | - borrowed value needs to live until here 

Observe que não há vidas explícitas nesse pedaço de código, exceto a definição da estrutura. O compilador é perfeitamente capaz de inferir vidas úteis em main() .

Nas definições de tipo, no entanto, as vidas úteis explícitas são inevitáveis. Por exemplo, existe uma ambiguidade aqui:

 struct RefPair(&u32, &u32); 

Estas devem ser vidas diferentes ou devem ser as mesmas? Importa da perspectiva de uso, struct RefPair< 'a, 'b>(&'a u32, &'b u32) é muito diferente de struct RefPair< 'a>(&'a u32, &'a u32) .

Agora, para casos simples, como o que você forneceu, o compilador poderia, teoricamente, elidir vidas como em outros lugares, mas esses casos são muito limitados e não valem complexidade extra no compilador, e esse ganho de clareza seria no muito menos questionável.

A anotação da vida útil na seguinte estrutura:

 struct Foo< 'a> { x: &'a i32, } 

especifica que uma ocorrência de Foo não deve sobreviver à referência que ela contém (campo x ).

O exemplo que você encontrou no livro Rust não ilustra isso porque as variables f e y ficam fora do escopo ao mesmo tempo.

Um exemplo melhor seria este:

 fn main() { let f : Foo; { let n = 5; // variable that is invalid outside this block let y = &n; f = Foo { x: y }; }; println!("{}", fx); } 

Agora, f realmente supera a variável apontada por fx .

O caso do livro é muito simples por design. O tema das vidas é considerado complexo.

O compilador não pode inferir facilmente o tempo de vida em uma function com vários argumentos.

Além disso, minha própria checkbox opcional tem um tipo OptionBool com um método as_slice cuja assinatura é na verdade:

 fn as_slice(&self) -> &'static [bool] { ... } 

Não há absolutamente nenhuma maneira de o compilador ter percebido isso.

Se uma function receber duas referências como argumentos e retornar uma referência, a implementação da function poderá, às vezes, retornar a primeira referência e, às vezes, 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, uma vez que cada referência de argumento pode referir-se a uma binding de variável diferente com uma vida útil diferente. Vidas explícitas ajudam a evitar ou esclarecer tal situação.

Da mesma forma, se uma estrutura contiver duas referências (como dois campos de membros), uma function de membro da estrutura pode algumas vezes retornar a primeira referência e, às vezes, a segunda. Novamente as vidas explícitas impedem tais ambigüidades.

Em algumas situações simples, há elisão vitalícia em que o compilador pode inferir vidas úteis.

Eu encontrei outra grande explicação aqui: http://doc.rust-lang.org/0.12.0/guide-lifetimes.html#returning-references .

Em geral, só é possível retornar referências se elas forem derivadas de um parâmetro para o procedimento. Nesse caso, o resultado do ponteiro sempre terá a mesma vida útil de um dos parâmetros; vidas nomeadas indicam qual parâmetro é.

Como recém-chegado a Rust, meu entendimento é que vidas explícitas servem a dois propósitos.

  1. Colocar uma anotação de tempo de vida explícita em uma function restringe o tipo de código que pode aparecer dentro dessa function. Tempos de vida explícitos permitem que o compilador garanta que seu programa esteja fazendo o que você pretendia.

  2. Se você (o compilador) quiser verificar se um pedaço de código é válido, você (o compilador) não terá que olhar iterativamente dentro de cada function chamada. É suficiente dar uma olhada nas annotations de funções que são chamadas diretamente por esse pedaço de código. Isso torna seu programa muito mais fácil de raciocinar para você (o compilador) e torna os tempos de compilation fáceis de gerenciar.

No ponto 1., considere o seguinte programa escrito em Python:

 import pandas as pd import numpy as np def second_row(ar): return ar[0] def work(second): df = pd.DataFrame(data=second) df.loc[0, 0] = 1 def main(): # .. load data .. ar = np.array([[0, 0], [0, 0]]) # .. do some work on second row .. second = second_row(ar) work(second) # .. much later .. print(repr(ar)) if __name__=="__main__": main() 

qual imprimirá

 array([[1, 0], [0, 0]]) 

Esse tipo de comportamento sempre me surpreende. O que está acontecendo é que o df está compartilhando memory com ar , então quando algum do conteúdo do df muda no work , essa mudança também infecta o ar . No entanto, em alguns casos, isso pode ser exatamente o que você deseja, por motivos de eficiência de memory (sem cópia). O problema real neste código é que a function second_row está retornando a primeira linha em vez da segunda; boa sorte depurando isso.

Considere, em vez disso, um programa similar escrito em Rust:

 #[derive(Debug)] struct Array< 'a, 'b>(&'a mut [i32], &'b mut [i32]); impl< 'a, 'b> Array< 'a, 'b> { fn second_row(&mut self) -> &mut &'b mut [i32] { &mut self.0 } } fn work(second: &mut [i32]) { second[0] = 1; } fn main() { // .. load data .. let ar1 = &mut [0, 0][..]; let ar2 = &mut [0, 0][..]; let mut ar = Array(ar1, ar2); // .. do some work on second row .. { let second = ar.second_row(); work(second); } // .. much later .. println!("{:?}", ar); } 

Compilando isso, você obtém

 error[E0308]: mismatched types --> src/main.rs:6:13 | 6 | &mut self.0 | ^^^^^^^^^^^ lifetime mismatch | = note: expected type `&mut &'b mut [i32]` found type `&mut &'a mut [i32]` note: the lifetime 'b as defined on the impl at 4:5... --> src/main.rs:4:5 | 4 | impl< 'a, 'b> Array< 'a, 'b> { | ^^^^^^^^^^^^^^^^^^^^^^^^^^ note: ...does not necessarily outlive the lifetime 'a as defined on the impl at 4:5 --> src/main.rs:4:5 | 4 | impl< 'a, 'b> Array< 'a, 'b> { | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 

Na verdade você tem dois erros, há também um com os papéis de 'a 'b e 'b intercambiados. Olhando para a anotação de second_row , descobrimos que a saída deve ser &mut &'b mut [i32] , sendo a saída supostamente uma referência a uma referência com tempo 'b vida 'b (a duração da segunda linha de Array ) . No entanto, como estamos retornando a primeira linha (que tem tempo 'a vida 'a ), o compilador reclama de incompatibilidade de tempo de vida. No lugar certo. No tempo certo. A debugging é uma brisa.

A razão pela qual o seu exemplo não funciona é simplesmente porque o Rust tem apenas vida útil local e inferência de tipos. O que você está sugerindo exige inferência global. Sempre que você tiver uma referência cuja vida não possa ser eliminada, ela deve ser anotada.