Vejamos uma implementação simples disso :
struct Parent {
count: u32,
}
struct Child<'a> {
parent: &'a Parent,
}
struct Combined<'a> {
parent: Parent,
child: Child<'a>,
}
impl<'a> Combined<'a> {
fn new() -> Self {
let parent = Parent { count: 42 };
let child = Child { parent: &parent };
Combined { parent, child }
}
}
fn main() {}
Isso falhará com o erro:
error[E0515]: cannot return value referencing local variable `parent`
--> src/main.rs:19:9
|
17 | let child = Child { parent: &parent };
| ------- `parent` is borrowed here
18 |
19 | Combined { parent, child }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function
error[E0505]: cannot move out of `parent` because it is borrowed
--> src/main.rs:19:20
|
14 | impl<'a> Combined<'a> {
| -- lifetime `'a` defined here
...
17 | let child = Child { parent: &parent };
| ------- borrow of `parent` occurs here
18 |
19 | Combined { parent, child }
| -----------^^^^^^---------
| | |
| | move out of `parent` occurs here
| returning this value requires that `parent` is borrowed for `'a`
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:
let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000
Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?
O que deveria acontecer child
? Se o valor foi movido apenas como parent
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:
{ // 0
let parent = Parent { count: 42 }; // 1
let child = Child { parent: &parent }; // 2
// 3
Combined { parent, child } // 4
} // 5
O tempo de vida concretoparent
de 1 a 4, inclusive (que representarei como [1,4]
). A vida útil concreta de child
é [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 retornar child
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:
impl Parent {
fn child(&self) -> Child { /* ... */ }
}
Isso usa elisão vitalícia para evitar a gravação de parâmetros genéricos explícitos da vida útil . É equivalente a:
impl Parent {
fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}
Nos dois casos, o método diz que Child
será retornada uma estrutura que foi parametrizada com a vida útil do concreto
self
. Dito de outra maneira, a Child
instância contém uma referência ao Parent
que a criou e, portanto, não pode viver mais que essa
Parent
instância.
Isso também nos permite reconhecer que algo está realmente errado com nossa função de criação:
fn make_combined<'a>() -> Combined<'a> { /* ... */ }
Embora seja mais provável que você veja isso escrito de uma forma diferente:
impl<'a> Combined<'a> {
fn new() -> Combined<'a> { /* ... */ }
}
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
ou Arc
.
Mais Informações
Depois de passar parent
para a estrutura, por que o compilador não pode obter uma nova referência parent
e atribuí-la à child
estrutura?
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:
let a = Object::new();
let b = a;
let c = b;
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:
#[derive(Debug)]
struct WhatAboutThis<'a> {
name: String,
nickname: Option<&'a str>,
}
fn main() {
let mut tricky = WhatAboutThis {
name: "Annabelle".to_string(),
nickname: None,
};
tricky.nickname = Some(&tricky.name[..4]);
println!("{:?}", tricky);
}
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:
fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }
Que tal Pin
?
Pin
, estabilizado no Rust 1.33, tem isso na documentação do módulo :
Um excelente exemplo desse cenário seria a construção de estruturas auto-referenciais, pois mover um objeto com ponteiros para si próprio os invalidará, o que poderia causar um comportamento indefinido.
É 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):
Não podemos informar o compilador sobre isso com uma referência normal, pois esse padrão não pode ser descrito com as regras usuais de empréstimo. Em vez disso , usamos um ponteiro bruto , embora seja conhecido por não ser nulo, pois sabemos que ele está apontando para a string.
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:
Parent
eChild
poderia ajudar ...