lifetime borrow-checker rust

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



borrow-checker rust (2)

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?


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 . Al desarrollar algo más grande que un proyecto de prueba, es tentador tratar de poner la Session y el Channel obtenidos de esa sesión juntos en una estructura, ocultando los detalles de implementación del usuario. Sin embargo, tenga en cuenta que la definición del Channel tiene la duración de ''sess en su anotación de tipo, mientras que la Session no.

Esto provoca errores de compilación similares relacionados con la vida útil.

Una forma de resolverlo de una manera muy simple es declarar la Session fuera de 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 el objeto especial para este propósito exacto: OwningHandle . Para evitar que el objeto subyacente se mueva, lo asignamos al montón utilizando un Box , 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 usar la Session , pero se almacena junto con el Channel que usaremos. Debido a que el objeto OwningHandle desreferencia a Box , que desreferencia a Channel , cuando lo almacena en una estructura, lo OwningHandle como tal. NOTA: Esto es solo mi entendimiento. OwningHandle esto puede no ser correcto, ya que parece estar bastante cerca de la discusión sobre la OwningHandle OwningHandle .

Un detalle curioso aquí es que la Session lógicamente tiene una relación similar con TcpStream que el Channel tiene con la Session , sin embargo, su propiedad no se toma y no hay anotaciones de tipo al respecto. En cambio, depende del usuario ocuparse de esto, como dice la documentación del método de handshake de handshake :

Esta sesión no toma posesión del socket proporcionado, se recomienda asegurarse de que el socket persista durante la vida útil de esta 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 uso de TcpStream , depende completamente del programador asegurar la exactitud del código. Con OwningHandle , la atención sobre dónde sucede la "magia peligrosa" se dibuja usando el bloque unsafe {} .

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.


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 cómo se representan los valores en la memoria y qué sucede cuando mueve esos valores. Anotemos Combined::new con 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 pasarle al child ? Si el valor solo se movió como lo hizo el parent , 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 era 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".

Anotemos Combined::new con 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 los parent es de 1 a 4, inclusive (que representaré como [1,4] ). La vida útil concreta del child es [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 del child sí 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 tratamos de devolver al child del 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 se devolverá una estructura Child que se ha parametrizado con la vida útil concreta de self . Dicho de otra manera, la instancia secundaria contiene una referencia al elemento Parent que la creó y, por lo tanto, no puede vivir más que esa instancia principal.

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 con la que se parametrizará Combined 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 el ''static tiempo de vida ''static y no hay forma de cumplir con esa condición.

¿Cómo lo soluciono?

La solución más fácil y más 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 utiliza 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 sigues el puntero.

La caja de alquiler o la caja owning_ref 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:

  • ¿Existe una versión propia de String :: chars?
  • Devolver un RWLockReadGuard independientemente de un método
  • ¿Cómo puedo devolver un iterador sobre un miembro de estructura bloqueada en Rust?
  • ¿Cómo devolver una referencia a un subvalor de un valor que está bajo un mutex?
  • ¿Cómo almaceno un resultado usando la deserialización Serde Zero-copy de un Hyper Chunk habilitado para Futures?
  • ¿Cómo almacenar una referencia sin tener que lidiar con vidas?

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

Más información

Después de mover el parent a la estructura, ¿por qué el compilador no puede obtener una nueva referencia al parent y asignarla al child en 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 embargo, debe usar algo como Option para 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 sobre eso con una referencia normal, ya que este patrón no puede describirse con las reglas habituales de endeudamiento. 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 Pin agrega a la tabla es una forma común de afirmar que se garantiza que un valor dado no se moverá.

Ver también:

  • ¿Cómo usar la estructura Pin con estructuras autorreferenciales?