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?
lifetime
borrow-checker
rust
Shepmaster
fuente
fuente
Parent
yChild
podría ayudar ...Respuestas:
Veamos una implementación simple de esto :
Esto fallará con el error:
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::new
con algunas direcciones de memoria hipotéticas que muestran dónde se encuentran los valores:¿Qué debería pasar
child
? Si el valor simplemente se movió comoparent
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::new
con algunos números de línea que usaremos para resaltar vidas:La vida útil concreta de
parent
es de 1 a 4, inclusive (que representaré como[1,4]
). La vida útil concreta dechild
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 útil en
child
sí 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 regresarchild
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í:Esto utiliza la elisión de por vida para evitar escribir parámetros de vida genéricos explícitos . Es equivalente a:
En ambos casos, el método dice que
Child
se devolverá una estructura que se ha parametrizado con la vida útil concreta deself
. Dicho de otra manera, laChild
instancia contiene una referencia a laParent
que la creó y, por lo tanto, no puede vivir más que esaParent
instancia.Esto también nos permite reconocer que algo está realmente mal con nuestra función de creación:
Aunque es más probable que vea esto escrito en una forma diferente:
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
Combined
se 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'static
duració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
Rc
oArc
.Más información
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:
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
Option
embargo, debe usar algo como hacerlo en dos pasos: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:
¿Qué hay de
Pin
?Pin
, estabilizado en Rust 1.33, tiene esto en la documentación del módulo :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):
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
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:
fuente
Combined
posee elChild
que posee elParent
. Eso puede o no tener sentido dependiendo de los tipos reales que tenga. Devolver referencias a sus propios datos internos es bastante típico.Pin
es 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.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
Session
yChannel
obtenido 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 laChannel
definición tiene la'sess
duración en su anotación de tipo, mientrasSession
que no.Esto provoca errores de compilación similares relacionados con las vidas.
Una forma de resolverlo de una manera muy simple es declarar el
Session
exterior 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 aBox
, lo que nos da la siguiente solución posible:El resultado de este código es que ya no podemos usarlo
Session
, pero se almacena junto con elChannel
que usaremos . Debido a que elOwningHandle
objeto hace referencia aBox
, que hace referencia aChannel
, 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 laOwningHandle
inseguridad .Un detalle curioso aquí es que
Session
lógicamente tiene una relación similar con laTcpStream
queChannel
tiene que serSession
, 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 :Entonces, con el
TcpStream
uso, depende completamente del programador garantizar la exactitud del código. Con elOwningHandle
, la atención sobre dónde sucede la "magia peligrosa" se dibuja usando elunsafe {}
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.
fuente