Por que não consigo armazenar um valor e uma referência a esse valor na mesma estrutura?

222

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?

Shepmaster
fonte
1
Para este último exemplo, uma definição de Parente Childpoderia ajudar ...
Matthieu M.
1
@MatthieuM. Eu debati isso, mas decidi contra, com base nas duas questões vinculadas. Nenhuma dessas perguntas analisou a definição da estrutura ou o método em questão, então pensei que seria melhor imitar que, para que as pessoas possam corresponder mais facilmente essa pergunta à sua própria situação. Nota que eu faça mostrar a assinatura do método na resposta.
Shepmaster 31/08/2015

Respostas:

245

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::newalguns 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::newalguns 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 childsi é [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 childdo 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 Childserá retornada uma estrutura que foi parametrizada com a vida útil do concreto self. Dito de outra maneira, a Childinstância contém uma referência ao Parentque a criou e, portanto, não pode viver mais que essa Parentinstâ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 Combinedserá parametrizada não é restringida por nada - pode ser o que o chamador quiser. Isso não faz sentido, porque o chamador pode especificar a 'staticvida ú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 Rcou Arc.

Mais Informações

Depois de passar parentpara a estrutura, por que o compilador não pode obter uma nova referência parente atribuí-la à childestrutura?

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 Optionfazê-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 Pinadiciona à tabela é uma maneira comum de afirmar que um determinado valor é garantido para não se mover.

Veja também:

Shepmaster
fonte
1
Algo assim ( is.gd/wl2IAt ) é considerado idiomático? Ou seja, para expor os dados por métodos em vez dos dados brutos.
Peter Hall
2
@ PeterHall claro, isso significa apenas que Combinedpossui o Childque possui o Parent. 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.
Shepmaster 4/16
Qual é a solução para o problema de heap?
derekdreery
@derekdreery talvez você possa expandir seu comentário? Por que o parágrafo inteiro está falando sobre a caixa owning_ref insuficiente?
Shepmaster #
1
@FynnBecker ainda é impossível armazenar uma referência e um valor para essa referência. 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.
Shepmaster
4

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 Sessione Channelobtido dessa sessão juntos em uma estrutura, ocultando os detalhes da implementação do usuário. No entanto, observe que a Channeldefinição tem a 'sessvida útil em sua anotação de tipo, enquanto Sessionnão.

Isso causa erros similares do compilador relacionados à vida útil.

Uma maneira de resolvê-lo de uma maneira muito simples é declarar a parte Sessionexterna 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 a Box, o que nos fornece a seguinte solução possível:

use ssh2::{Channel, Error, Session};
use std::net::TcpStream;

use owning_ref::OwningHandle;

struct DeviceSSHConnection {
    tcp: TcpStream,
    channel: OwningHandle<Box<Session>, Box<Channel<'static>>>,
}

impl DeviceSSHConnection {
    fn new(targ: &str, c_user: &str, c_pass: &str) -> Self {
        use std::net::TcpStream;
        let mut session = Session::new().unwrap();
        let mut tcp = TcpStream::connect(targ).unwrap();

        session.handshake(&tcp).unwrap();
        session.set_timeout(5000);
        session.userauth_password(c_user, c_pass).unwrap();

        let mut sess = Box::new(session);
        let mut oref = OwningHandle::new_with_fn(
            sess,
            unsafe { |x| Box::new((*x).channel_session().unwrap()) },
        );

        oref.shell().unwrap();
        let ret = DeviceSSHConnection {
            tcp: tcp,
            channel: oref,
        };
        ret
    }
}

O resultado desse código é que não podemos mais usá- Sessionlo, mas ele é armazenado juntamente com o Channelque usaremos. Como o OwningHandleobjeto desreferencia a Box, ao qual desreferencia Channel, 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 sobre OwningHandleinsegurança .

Um detalhe curioso aqui é que a Sessionlógica tem uma relação semelhante com TcpStreamcomo Channeltem que Session, 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 :

Esta sessão não possui a propriedade do soquete fornecido, é recomendável garantir que o soquete persista o tempo de vida desta sessão para garantir que a comunicação seja executada corretamente.

Também é altamente recomendável que o fluxo fornecido não seja usado simultaneamente em outro lugar durante a sessão, pois pode interferir no protocolo.

Portanto, com o TcpStreamuso, cabe totalmente ao programador garantir a correção do código. Com o OwningHandle, a atenção para onde a "mágica perigosa" acontece é atraída usando o unsafe {}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.

Andrew Y
fonte