¿Por qué se necesitan vidas útiles explícitas en Rust?

Resuelto corazza asked hace 9 años • 11 respuestas

Estaba leyendo el capítulo sobre vidas del libro Rust y encontré este ejemplo para una vida con nombre/explícita:

struct Foo<'a> {
    x: &'a i32,
}

fn main() {
    let x;                    // -+ x goes into scope
                              //  |
    {                         //  |
        let y = &5;           // ---+ y goes into scope
        let f = Foo { x: y }; // ---+ f goes into scope
        x = &f.x;             //  | | error here
    }                         // ---+ f and y go out of scope
                              //  |
    println!("{}", x);        //  |
}                             // -+ x goes out of scope

Para mí está bastante claro que el error que evita el compilador es el uso después de la liberación de la referencia asignada a x: después de que se completa el alcance interno fy, por lo tanto &f.x, deja de ser válida y no debería haberse asignado a x.

Mi problema es que el problema podría haberse analizado fácilmente sin utilizar la vida útil explícita 'a , por ejemplo, infiriendo una asignación ilegal de una referencia a un alcance más amplio ( x = &f.x;).

¿En qué casos se necesitan realmente tiempos de vida explícitos para evitar errores de uso después de la liberación (o de alguna otra clase)?

corazza avatar Jul 24 '15 18:07 corazza
Aceptado

Todas las otras respuestas tienen puntos destacados ( el ejemplo concreto de fjh donde se necesita una vida útil explícita ), pero les falta una cosa clave: ¿por qué se necesitan vidas útiles explícitas cuando el compilador le dirá que se equivocó ?

En realidad, esta es la misma pregunta que "¿por qué se necesitan tipos explícitos cuando el compilador puede inferirlos?". Un ejemplo hipotético:

fn foo() -> _ {  
    ""
}

Por supuesto, el compilador puede ver que estoy devolviendo un archivo &'static str, entonces, ¿por qué el programador tiene que escribirlo?

La razón principal es que, si bien el compilador puede ver lo que hace su código, no sabe cuál fue su intención.

Las funciones son un límite natural para el cortafuegos y los efectos del cambio de código. Si permitiéramos que las duraciones se inspeccionaran completamente desde el código, entonces un cambio aparentemente inocente podría afectar las duraciones, lo que podría causar errores en una función lejana. Este no es un ejemplo hipotético. Según tengo entendido, Haskell tiene este problema cuando confías en la inferencia de tipos para funciones de nivel superior. Rust cortó ese problema particular de raíz.

También hay un beneficio de eficiencia para el compilador: solo es necesario analizar las firmas de funciones para verificar los tipos y la duración. Más importante aún, tiene un beneficio de eficiencia para el programador. Si no tuviéramos tiempos de vida explícitos, ¿qué hace esta función?

fn foo(a: &u8, b: &u8) -> &u8

Es imposible saberlo sin inspeccionar la fuente, lo que iría en contra de una gran cantidad de mejores prácticas de codificación.

al inferir una asignación ilegal de una referencia a un ámbito más amplio

Los alcances son vidas, esencialmente. Un poco más claramente, una vida útil 'aes un parámetro de vida útil genérico que se puede especializar con un alcance específico en el momento de la compilación, según el sitio de llamada.

¿Se necesitan realmente tiempos de vida explícitos para evitar [...] errores?

De nada. Se necesitan tiempos de vida para evitar errores, pero se necesitan tiempos de vida explícitos para proteger la poca cordura que tienen los programadores.

Shepmaster avatar Jul 24 '2015 13:07 Shepmaster

Echemos un vistazo al siguiente ejemplo.

fn foo<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
    x
}

fn main() {
    let x = 12;
    let z: &u32 = {
        let y = 42;
        foo(&x, &y)
    };
}

Aquí, las vidas explícitas son importantes. Esto se compila porque el resultado de footiene la misma duración que su primer argumento ( 'a), por lo que puede sobrevivir a su segundo argumento. Esto se expresa mediante los nombres de por vida en la firma de foo. Si cambiara los argumentos en la llamada al foocompilador, se quejaría de que yno dura lo suficiente:

error[E0597]: `y` does not live long enough
  --> src/main.rs:10:5
   |
9  |         foo(&y, &x)
   |              - borrow occurs here
10 |     };
   |     ^ `y` dropped here while still borrowed
11 | }
   | - borrowed value needs to live until here
fjh avatar Jul 24 '2015 11:07 fjh

La anotación de por vida en la siguiente estructura:

struct Foo<'a> {
    x: &'a i32,
}

especifica que una Fooinstancia no debe sobrevivir a la referencia que contiene ( xcampo).

El ejemplo que encontró en el libro de Rust no ilustra esto porque las variables fy ysalen del alcance al mismo tiempo.

Un mejor ejemplo sería este:

fn main() {
    let f : Foo;
    {
        let n = 5;  // variable that is invalid outside this block
        let y = &n;
        f = Foo { x: y };
    };
    println!("{}", f.x);
}

Ahora, frealmente sobrevive a la variable señalada por f.x.

user3151599 avatar Jul 25 '2015 10:07 user3151599

Tenga en cuenta que no hay tiempos de vida explícitos en ese fragmento de código, excepto la definición de estructura. El compilador es perfectamente capaz de inferir la duración de los archivos main().

Sin embargo, en las definiciones de tipos, las duraciones explícitas son inevitables. Por ejemplo, aquí hay una ambigüedad:

struct RefPair(&u32, &u32);

¿Deberían ser vidas diferentes o deberían ser iguales? Sí importa desde la perspectiva del uso, struct RefPair<'a, 'b>(&'a u32, &'b u32)es muy diferente de struct RefPair<'a>(&'a u32, &'a u32).

Ahora, para casos simples, como el que usted proporcionó, el compilador teóricamente podría eludir tiempos de vida como lo hace en otros lugares, pero tales casos son muy limitados y no merecen una complejidad adicional en el compilador, y esta ganancia en claridad estaría en el lo menos cuestionable.

Vladimir Matveev avatar Jul 24 '2015 11:07 Vladimir Matveev