¿Por qué no puedo almacenar un valor y una referencia a ese valor en la misma estructura?

222

Tengo un valor y quiero almacenar ese valor y una referencia a algo dentro de ese valor en mi propio 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)
}

A veces, tengo un valor y quiero almacenar ese valor y una referencia a ese valor en la misma estructura:

struct Combined<'a>(Thing, &'a Thing);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing::new();

    Combined(thing, &thing)
}

A veces, ni siquiera estoy tomando una referencia del valor y obtengo el mismo error:

struct Combined<'a>(Parent, Child<'a>);

fn make_combined<'a>() -> Combined<'a> {
    let parent = Parent::new();
    let child = parent.child();

    Combined(parent, child)
}

En cada uno de estos casos, recibo un error de que uno de los valores "no vive lo suficiente". ¿Qué significa este error?

Shepmaster
fuente
1
Para el último ejemplo, una definición de Parenty Childpodría ayudar ...
Matthieu M.
1
@MatthieuM. Lo debatí, pero decidí no hacerlo en base a las dos preguntas vinculadas. Ninguna de esas preguntas examinó la definición de la estructura o el método en cuestión, por lo que pensé que sería mejor imitar eso para que las personas puedan relacionar más fácilmente esta pregunta con su propia situación. Tenga en cuenta que muestro la firma del método en la respuesta.
Shepmaster

Respuestas:

245

Veamos una implementación simple de esto :

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() {}

Esto fallará con el error:

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 comprender completamente este error, debe pensar en cómo se representan los valores en la memoria y qué sucede cuando mueve esos valores. Hagamos anotaciones Combined::newcon algunas direcciones de memoria hipotéticas que muestran dónde se encuentran los valores:

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 ... ?

¿Qué debería pasar child? Si el valor simplemente se movió como parent era, entonces se referiría a la memoria que ya no se garantiza que tenga un valor válido. Cualquier otro fragmento de código puede almacenar valores en la dirección de memoria 0x1000. Acceder a esa memoria suponiendo que fuera un número entero podría provocar bloqueos y / o errores de seguridad, y es una de las principales categorías de errores que evita Rust.

Este es exactamente el problema que previenen las vidas . Una vida útil es un poco de metadatos que le permite a usted y al compilador saber cuánto tiempo será válido un valor en su ubicación de memoria actual . Esa es una distinción importante, ya que es un error común que cometen los recién llegados de Rust. ¡Las vidas de óxido no son el período de tiempo entre el momento en que se crea un objeto y cuando se destruye!

Como analogía, piense de esta manera: durante la vida de una persona, residirá en muchos lugares diferentes, cada uno con una dirección distinta. Una vida útil de Rust se refiere a la dirección en la que reside actualmente , no a la fecha en que va a morir en el futuro (aunque morir también cambia su dirección). Cada vez que te mudas es relevante porque tu dirección ya no es válida.

También es importante tener en cuenta que las vidas no cambian su código; su código controla las vidas, sus vidas no controlan el código. El dicho esencial es "las vidas son descriptivas, no prescriptivas".

Hagamos anotaciones Combined::newcon algunos números de línea que usaremos para resaltar vidas:

{                                          // 0
    let parent = Parent { count: 42 };     // 1
    let child = Child { parent: &parent }; // 2
                                           // 3
    Combined { parent, child }             // 4
}                                          // 5

La vida útil concreta de parentes de 1 a 4, inclusive (que representaré como [1,4]). La vida útil concreta de childes [2,4], y la vida útil concreta del valor de retorno es [4,5]. Es posible tener tiempos de vida concretos que comienzan en cero, lo que representaría la vida útil de un parámetro para una función o algo que existía fuera del bloque.

Tenga en cuenta que la vida útil en childsí misma es [2,4], pero que se refiere a un valor con una vida útil de [1,4]. Esto está bien siempre que el valor de referencia se vuelva inválido antes que el valor de referencia. El problema ocurre cuando intentamos regresar childdel bloque. Esto "extendería" la vida útil más allá de su longitud natural.

Este nuevo conocimiento debería explicar los dos primeros ejemplos. El tercero requiere mirar la implementación de Parent::child. Lo más probable es que se vea más o menos así:

impl Parent {
    fn child(&self) -> Child { /* ... */ }
}

Esto utiliza la elisión de por vida para evitar escribir parámetros de vida genéricos explícitos . Es equivalente a:

impl Parent {
    fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}

En ambos casos, el método dice que Childse devolverá una estructura que se ha parametrizado con la vida útil concreta de self. Dicho de otra manera, la Childinstancia contiene una referencia a la Parentque la creó y, por lo tanto, no puede vivir más que esa Parentinstancia.

Esto también nos permite reconocer que algo está realmente mal con nuestra función de creación:

fn make_combined<'a>() -> Combined<'a> { /* ... */ }

Aunque es más probable que vea esto escrito en una forma diferente:

impl<'a> Combined<'a> {
    fn new() -> Combined<'a> { /* ... */ }
}

En ambos casos, no se proporciona ningún parámetro de por vida a través de un argumento. Esto significa que la vida útil que Combinedse parametrizará no está limitada por nada, puede ser lo que la persona que llama quiera que sea. Esto no tiene sentido, porque la persona que llama podría especificar la 'staticduración y no hay forma de cumplir con esa condición.

¿Cómo lo soluciono?

La solución más fácil y recomendada es no intentar juntar estos elementos en la misma estructura. Al hacer esto, su anidamiento de estructura imitará las vidas de su código. Coloque tipos que posean datos en una estructura juntos y luego proporcione métodos que le permitan obtener referencias u objetos que contengan referencias según sea necesario.

Hay un caso especial en el que el seguimiento de por vida es demasiado celoso: cuando tienes algo colocado en el montón. Esto ocurre cuando usa un Box<T>, por ejemplo. En este caso, la estructura que se mueve contiene un puntero en el montón. El valor señalado permanecerá estable, pero la dirección del puntero se moverá. En la práctica, esto no importa, ya que siempre sigue el puntero.

La caja de alquiler (YA NO SE MANTENERÁ O APOYARÁ) o la caja de propiedad de la propiedad son formas de representar este caso, pero requieren que la dirección base nunca se mueva . Esto descarta los vectores mutantes, que pueden causar una reasignación y un movimiento de los valores asignados en el montón.

Ejemplos de problemas resueltos con Rental:

En otros casos, es posible que desee pasar a algún tipo de recuento de referencias, como mediante el uso de Rco Arc.

Más información

Después de pasar parenta la estructura, ¿por qué el compilador no puede obtener una nueva referencia parenty asignarla childen la estructura?

Si bien es teóricamente posible hacer esto, hacerlo introduciría una gran cantidad de complejidad y sobrecarga. Cada vez que se mueve el objeto, el compilador necesitaría insertar código para "arreglar" la referencia. Esto significaría que copiar una estructura ya no es una operación muy barata que solo mueve algunos bits. Incluso podría significar que un código como este es costoso, dependiendo de cuán bueno sea un optimizador hipotético:

let a = Object::new();
let b = a;
let c = b;

En lugar de obligar a que esto suceda para cada movimiento, el programador puede elegir cuándo ocurrirá mediante la creación de métodos que tomarán las referencias apropiadas solo cuando los llame.

Un tipo con una referencia a sí mismo

Hay un caso específico en el que puede crear un tipo con una referencia a sí mismo. Sin Optionembargo, debe usar algo como hacerlo en dos pasos:

#[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);
}

Esto funciona, en cierto sentido, pero el valor creado está altamente restringido: nunca se puede mover. En particular, esto significa que no se puede devolver de una función o pasar por valor a nada. Una función de constructor muestra el mismo problema con las vidas que el anterior:

fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }

¿Qué hay de Pin?

Pin, estabilizado en Rust 1.33, tiene esto en la documentación del módulo :

Un buen ejemplo de tal escenario sería construir estructuras autorreferenciales, ya que mover un objeto con punteros a sí mismo los invalidará, lo que podría causar un comportamiento indefinido.

Es importante tener en cuenta que "autorreferencial" no significa necesariamente usar una referencia . De hecho, el ejemplo de una estructura autorreferencial dice específicamente (énfasis mío):

No podemos informar al compilador acerca de eso con una referencia normal, ya que este patrón no se puede describir con las reglas de préstamo habituales. En su lugar , usamos un puntero sin formato , aunque se sabe que no es nulo, ya que sabemos que apunta a la cadena.

La capacidad de utilizar un puntero sin formato para este comportamiento existe desde Rust 1.0. De hecho, el propietario de ref y el alquiler utilizan punteros en bruto bajo el capó.

Lo único que se Pinagrega a la tabla es una forma común de afirmar que se garantiza que un valor dado no se moverá.

Ver también:

Shepmaster
fuente
1
¿Es algo como esto ( is.gd/wl2IAt ) considerado idiomático? Es decir, exponer los datos a través de métodos en lugar de los datos sin procesar.
Peter Hall
2
@PeterHall seguro, solo significa que Combinedposee el Childque posee el Parent. Eso puede o no tener sentido dependiendo de los tipos reales que tenga. Devolver referencias a sus propios datos internos es bastante típico.
Shepmaster el
¿Cuál es la solución al problema del montón?
derekdreery
@derekdreery tal vez podría ampliar su comentario? ¿Por qué todo el párrafo que habla de la caja owning_ref es insuficiente?
Shepmaster
1
@FynnBecker todavía es imposible almacenar una referencia y un valor para esa referencia. Pines principalmente una forma de conocer la seguridad de una estructura que contiene un puntero autorreferencial . La capacidad de utilizar un puntero sin formato para el mismo propósito existe desde Rust 1.0.
Shepmaster
4

Un problema ligeramente diferente que causa mensajes de compilación muy similares es la dependencia de la vida útil del objeto, en lugar de almacenar una referencia explícita. Un ejemplo de eso es la biblioteca ssh2 . En el desarrollo de algo más grande que un proyecto de prueba, es tentador tratar de poner el Sessiony Channelobtenido de esa sesión uno junto al otro en una estructura, ocultando los detalles de implementación por parte del usuario. Sin embargo, tenga en cuenta que la Channeldefinición tiene la 'sessduración en su anotación de tipo, mientras Sessionque no.

Esto provoca errores de compilación similares relacionados con las vidas.

Una forma de resolverlo de una manera muy simple es declarar el Sessionexterior en la persona que llama, y ​​luego anotar la referencia dentro de la estructura con una vida útil, similar a la respuesta en esta publicación del Foro de usuarios de Rust hablando sobre el mismo problema al encapsular SFTP . Esto no se verá elegante y puede no aplicarse siempre, ¡porque ahora tiene dos entidades con las que lidiar, en lugar de una que desea!

Resulta que la caja de alquiler o la caja owning_ref de la otra respuesta son las soluciones para este problema también. Consideremos el owning_ref, que tiene por objeto especial para este propósito exacto: OwningHandle. Para evitar que el objeto subyacente se mueva, lo asignamos al montón utilizando a Box, lo que nos da la siguiente solución posible:

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
    }
}

El resultado de este código es que ya no podemos usarlo Session, pero se almacena junto con el Channelque usaremos . Debido a que el OwningHandleobjeto hace referencia a Box, que hace referencia a Channel, al almacenarlo en una estructura, lo nombramos como tal. NOTA: Esto es solo mi entendimiento. Sospecho que esto puede no ser correcto, ya que parece estar bastante cerca de la discusión sobre la OwningHandleinseguridad .

Un detalle curioso aquí es que Sessionlógicamente tiene una relación similar con la TcpStreamque Channeltiene que ser Session, sin embargo, su propiedad no se toma y no hay anotaciones de tipo para hacerlo. En cambio, depende del usuario ocuparse de esto, como dice la documentación del método de apretón de manos :

Esta sesión no toma posesión del socket proporcionado, se recomienda asegurarse de que el socket persista durante toda la sesión para garantizar que la comunicación se realice correctamente.

También se recomienda encarecidamente que la transmisión proporcionada no se use simultáneamente en otro lugar durante la sesión, ya que puede interferir con el protocolo.

Entonces, con el TcpStreamuso, depende completamente del programador garantizar la exactitud del código. Con el OwningHandle, la atención sobre dónde sucede la "magia peligrosa" se dibuja usando el unsafe {}bloque.

Una discusión adicional y de más alto nivel sobre este tema se encuentra en este hilo del Foro de usuarios de Rust , que incluye un ejemplo diferente y su solución utilizando la caja de alquiler, que no contiene bloques inseguros.

Andrew Y
fuente