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 alparent
y asignarla alchild
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?