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::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 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::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 parent
es de 1 a 4, inclusive (que representaré como [1,4]
). La vida útil concreta de 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 ú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 regresar 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 Child
se devolverá una estructura que se ha parametrizado con la vida útil concreta de
self
. Dicho de otra manera, la Child
instancia contiene una referencia a la Parent
que la creó y, por lo tanto, no puede vivir más que esa
Parent
instancia.
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 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
o Arc
.
Más información
Después de pasar parent
a la estructura, ¿por qué el compilador no puede obtener una nueva referencia parent
y asignarla 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 Option
embargo, 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 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:
Parent
yChild
podría ayudar ...