Eu tenho um valor e quero armazenar esse valor e uma referência a algo dentro desse valor no meu próprio tipo:
struct Thing {
count: u32,
}
struct Combined<'a>(Thing, &'a u32);
fn make_combined<'a>() -> Combined<'a> {
let thing = Thing { count: 42 };
Combined(thing, &thing.count)
}
Às vezes, tenho um valor e quero armazenar esse valor e uma referência a esse valor na mesma estrutura:
struct Combined<'a>(Thing, &'a Thing);
fn make_combined<'a>() -> Combined<'a> {
let thing = Thing::new();
Combined(thing, &thing)
}
Às vezes, nem estou fazendo uma referência ao valor e recebo o mesmo erro:
struct Combined<'a>(Parent, Child<'a>);
fn make_combined<'a>() -> Combined<'a> {
let parent = Parent::new();
let child = parent.child();
Combined(parent, child)
}
Em cada um desses casos, recebo um erro de que um dos valores "não vive o suficiente". O que esse erro significa?
lifetime
borrow-checker
rust
Shepmaster
fonte
fonte
Parent
eChild
poderia ajudar ...Respostas:
Vejamos uma implementação simples disso :
Isso falhará com o erro:
Para entender completamente esse erro, você deve pensar em como os valores são representados na memória e o que acontece quando você os move . Vamos anotar
Combined::new
alguns endereços de memória hipotéticos que mostram onde os valores estão localizados:O que deveria acontecer
child
? Se o valor foi movido apenas comoparent
estava, ele se referiria à memória que não é mais garantida como tendo um valor válido. Qualquer outra parte do código pode armazenar valores no endereço de memória 0x1000. O acesso a essa memória, assumindo que fosse um número inteiro, pode levar a falhas e / ou bugs de segurança, e é uma das principais categorias de erros que o Rust impede.Este é exatamente o problema que as vidas impedem. A vida útil é um pouco de metadados que permitem que você e o compilador saibam por quanto tempo um valor será válido em seu local de memória atual . Essa é uma distinção importante, pois é um erro comum que os novatos da Rust cometem. As vidas úteis da ferrugem não são o período entre a criação e a destruição de um objeto!
Como analogia, pense dessa maneira: durante a vida de uma pessoa, ela residirá em muitos locais diferentes, cada um com um endereço distinto. Uma vida útil da Rust se preocupa com o endereço em que você reside atualmente , e não sempre que você morrer no futuro (embora morrer também mude seu endereço). Toda vez que você se move, é relevante porque seu endereço não é mais válido.
Também é importante observar que as vidas úteis não alteram seu código; seu código controla as existências, suas vidas não controlam o código. O ditado expressivo é "as vidas são descritivas, não prescritivas".
Vamos anotar
Combined::new
alguns números de linha que usaremos para destacar a vida útil:O tempo de vida concreto
parent
de 1 a 4, inclusive (que representarei como[1,4]
). A vida útil concreta dechild
é[2,4]
e a vida útil concreta do valor de retorno é[4,5]
. É possível ter tempos de vida concretos que começam em zero - isso representaria o tempo de vida de um parâmetro para uma função ou algo que existia fora do bloco.Observe que a vida útil em
child
si é[2,4]
, mas se refere a um valor com vida útil de[1,4]
. Isso é bom, desde que o valor de referência se torne inválido antes do valor referido. O problema ocorre quando tentamos retornarchild
do bloco. Isso "prolongaria demais" a vida útil além do seu comprimento natural.Esse novo conhecimento deve explicar os dois primeiros exemplos. O terceiro exige uma análise da implementação do
Parent::child
. Provavelmente, será algo parecido com isto:Isso usa elisão vitalícia para evitar a gravação de parâmetros genéricos explícitos da vida útil . É equivalente a:
Nos dois casos, o método diz que
Child
será retornada uma estrutura que foi parametrizada com a vida útil do concretoself
. Dito de outra maneira, aChild
instância contém uma referência aoParent
que a criou e, portanto, não pode viver mais que essaParent
instância.Isso também nos permite reconhecer que algo está realmente errado com nossa função de criação:
Embora seja mais provável que você veja isso escrito de uma forma diferente:
Nos dois casos, não há parâmetro de duração sendo fornecido por meio de um argumento. Isso significa que a vida útil que
Combined
será parametrizada não é restringida por nada - pode ser o que o chamador quiser. Isso não faz sentido, porque o chamador pode especificar a'static
vida útil e não há como atender a essa condição.Como faço para corrigir isso?
A solução mais fácil e mais recomendada é não tentar reunir esses itens na mesma estrutura. Ao fazer isso, seu aninhamento de estrutura imitará a vida útil do seu código. Coloque tipos que possuem dados em uma estrutura juntos e forneça métodos que permitem obter referências ou objetos que contenham referências, conforme necessário.
Há um caso especial em que o rastreamento vitalício é excessivamente zeloso: quando você coloca algo na pilha. Isso ocorre quando você usa um
Box<T>
, por exemplo. Nesse caso, a estrutura que é movida contém um ponteiro para o heap. O valor apontado permanecerá estável, mas o endereço do ponteiro se moverá. Na prática, isso não importa, pois você sempre segue o ponteiro.O engradado de aluguel (NÃO É MAIS MANTIDO OU SUPORTADO) ou o engradado owning_ref são formas de representar esse caso, mas exigem que o endereço base nunca se mova . Isso exclui vetores mutantes, o que pode causar uma realocação e uma movimentação dos valores alocados pela pilha.
Exemplos de problemas resolvidos com o Rental:
Em outros casos, você pode passar para algum tipo de contagem de referência, como usando
Rc
ouArc
.Mais Informações
Embora seja teoricamente possível fazer isso, isso introduziria uma grande quantidade de complexidade e sobrecarga. Sempre que o objeto é movido, o compilador precisaria inserir código para "consertar" a referência. Isso significa que copiar uma estrutura não é mais uma operação muito barata que apenas move alguns bits. Pode até significar que um código como esse é caro, dependendo de quão bom seria um otimizador hipotético:
Em vez de forçar isso a cada movimento, o programador escolhe quando isso acontecer, criando métodos que terão as referências apropriadas somente quando você as chamar.
Um tipo com uma referência a si mesmo
Há um caso específico em que você pode criar um tipo com uma referência a si mesmo. Você precisa usar algo como
Option
fazê-lo em duas etapas:Isso funciona, em certo sentido, mas o valor criado é altamente restrito - nunca pode ser movido. Notavelmente, isso significa que não pode ser retornado de uma função ou passado por valor para qualquer coisa. Uma função construtora mostra o mesmo problema com as vidas úteis acima:
Que tal
Pin
?Pin
, estabilizado no Rust 1.33, tem isso na documentação do módulo :É importante observar que "auto-referência" não significa necessariamente usar uma referência . De fato, o exemplo de uma estrutura auto-referencial diz especificamente (ênfase minha):
A capacidade de usar um ponteiro bruto para esse comportamento existe desde o Rust 1.0. De fato, o proprietário-ref e o aluguel usam indicadores brutos sob o capô.
A única coisa que
Pin
adiciona à tabela é uma maneira comum de afirmar que um determinado valor é garantido para não se mover.Veja também:
fonte
Combined
possui oChild
que possui oParent
. Isso pode ou não fazer sentido, dependendo dos tipos reais que você possui. Retornar referências aos seus próprios dados internos é bastante típico.Pin
é principalmente uma maneira de conhecer a segurança de uma estrutura que contém um ponteiro auto-referencial . A capacidade de usar um ponteiro bruto para o mesmo objetivo existe desde o Rust 1.0.Um problema um pouco diferente que causa mensagens muito semelhantes do compilador é a dependência da vida útil do objeto, em vez de armazenar uma referência explícita. Um exemplo disso é a biblioteca ssh2 . Ao desenvolver algo maior que um projeto de teste, é tentador colocar o
Session
eChannel
obtido dessa sessão juntos em uma estrutura, ocultando os detalhes da implementação do usuário. No entanto, observe que aChannel
definição tem a'sess
vida útil em sua anotação de tipo, enquantoSession
não.Isso causa erros similares do compilador relacionados à vida útil.
Uma maneira de resolvê-lo de uma maneira muito simples é declarar a parte
Session
externa no chamador e, em seguida, anotar a referência dentro da estrutura por toda a vida útil, semelhante à resposta neste post do Fórum do Usuário da Rust falando sobre o mesmo problema ao encapsular o SFTP . Isso não parecerá elegante e nem sempre será aplicável - porque agora você tem duas entidades para lidar, em vez de uma que você queria!Acontece que a caixa de aluguel ou a caixa owning_ref da outra resposta também são as soluções para esse problema. Vamos considerar o owning_ref, que tem o objeto especial para esta finalidade exata:
OwningHandle
. Para evitar a movimentação do objeto subjacente, alocamos-no no heap usando aBox
, o que nos fornece a seguinte solução possível:O resultado desse código é que não podemos mais usá-
Session
lo, mas ele é armazenado juntamente com oChannel
que usaremos. Como oOwningHandle
objeto desreferencia aBox
, ao qual desreferenciaChannel
, ao armazená-lo em uma estrutura, nós o denominamos como tal. NOTA: Este é apenas o meu entendimento. Suspeito que isso possa não estar correto, pois parece estar bem próximo da discussão sobreOwningHandle
insegurança .Um detalhe curioso aqui é que a
Session
lógica tem uma relação semelhante comTcpStream
comoChannel
tem queSession
, no entanto, sua propriedade não é tomada e não há anotações de tipo ao redor fazê-lo. Em vez disso, cabe ao usuário cuidar disso, como diz a documentação do método de handshake :Portanto, com o
TcpStream
uso, cabe totalmente ao programador garantir a correção do código. Com oOwningHandle
, a atenção para onde a "mágica perigosa" acontece é atraída usando ounsafe {}
bloco.Uma discussão mais aprofundada e mais de alto nível sobre esse assunto está tópico do Fórum do Usuário da Rust - que inclui um exemplo diferente e sua solução usando a caixa de aluguel, que não contém blocos inseguros.
fonte