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


Estaba leyendo el capítulo lifetimes del libro Rust, y me encontré con este ejemplo para una vida nombrada/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

Está bastante claro para mí que el error que está siendo prevenido por el compilador es el use-after-free de la referencia asignada a x: después de que el ámbito interno esté hecho, f y por lo tanto &f.x se vuelve inválido, y no debería haber sido asignado a x.

Mi problema es que el problema podría haber sido fácilmente analizado sin usando el explícito 'a vida, por ejemplo, al inferir una cesión ilegal de una referencia a un alcance más amplio (x = &f.x;).

¿En qué casos son realmente necesarias vidas explícitas para evitar el uso después de la liberación (o alguna otra clase?) errores?

Author: Boiethios, 2015-07-24

9 answers

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

Esta es en realidad 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 &'static str, así que ¿por qué el programador tener que escribirlo?

La razón principal es que mientras el compilador puede ver lo que hace tu código, no sabe cuál era tu intención.

Las funciones son un límite natural para cortafuegos los efectos del cambio de código. Si permitiéramos que las vidas sean completamente inspeccionadas desde el código, entonces un cambio de aspecto inocente podría afectar las vidas, 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íe en la inferencia de tipos para funciones de nivel superior. El óxido cortó ese problema en particular de raíz.

También hay un beneficio de eficiencia para el compilador - solo las firmas de función necesitan ser analizadas para verificar tipos y vidas. Más importante aún, tiene un beneficio de eficiencia para el programador. Si no tuviéramos vidas explícitas, qué hace esta función:

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

Es imposible decirlo sin inspeccionar la fuente, lo que iría en contra de un gran número de codificación mejores prácticas.

Al inferir una cesión ilegal de una referencia a un alcance más amplio

Los ámbitos son vidas, esencialmente. Un poco más claramente, un lifetime 'a es un parámetro genérico lifetime que puede ser especializado con un alcance específico en tiempo de compilación, basado en el sitio de la llamada.

Son vidas explícitas realmente necesarias para prevenir [...] errores?

Para nada. Se necesitan vidas para evitar errores, pero se necesitan vidas explícitas para proteger lo poco que tienen los programadores de cordura.

 172
Author: Shepmaster,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-10 22:22:46

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 compila porque el resultado de foo tiene la misma vida que su primer argumento ('a), por lo que puede sobrevivir a su segundo argumento. Esto se expresa por los nombres de vida en la firma de foo. Si cambias los argumentos en la llamada a foo el compilador se quejaría de que y no vive 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
 74
Author: fjh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-10 23:14:30

Tenga en cuenta que no hay vidas explícitas en esa pieza de código, excepto la definición de la estructura. El compilador es perfectamente capaz de inferir vidas en main().

En las definiciones de tipos, sin embargo, las vidas explícitas son inevitables. Por ejemplo, hay una ambigüedad aquí:

struct RefPair(&u32, &u32);

¿Deberían ser vidas diferentes o deberían ser las mismas? 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 siempre y cuando, el compilador podría teóricamente eludir vidas como lo hace en otros lugares, pero tales casos son muy limitados y no valen complejidad adicional en el compilador, y esta ganancia en claridad sería al menos cuestionable.

 8
Author: Vladimir Matveev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-10 23:26:18

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

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

Especifica que una instancia Foo no debe sobrevivir a la referencia que contiene (campox).

El ejemplo que encontró en el libro de Rust no ilustra esto porque las variables f y y salen 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, f realmente sobrevive a la variable señalada por f.x.

 8
Author: user3151599,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-27 14:49:11

El caso del libro es muy simple por diseño. El tema de las vidas se considera complejo.

El compilador no puede inferir fácilmente el tiempo de vida en una función con múltiples argumentos.

Además, mi propia caja opcional tiene un tipo OptionBool con un método as_slice cuya firma en realidad es:

fn as_slice(&self) -> &'static [bool] { ... }

No hay absolutamente ninguna manera en que el compilador podría haber descubierto eso.

 4
Author: llogiq,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-10 23:18:22

Si una función recibe dos referencias como argumentos y devuelve una referencia, entonces la implementación de la función a veces puede devolver la primera referencia y a veces la segunda. Es imposible predecir qué referencia se devolverá para una llamada dada. En este caso, es imposible inferir un tiempo de vida para la referencia devuelta, ya que cada referencia de argumento puede referirse a un enlace de variable diferente con un tiempo de vida diferente. Vidas explícitas ayudan a evitar o aclarar tal situación.

Del mismo modo, si una estructura contiene dos referencias (como dos campos miembro), entonces una función miembro de la estructura a veces puede devolver la primera referencia y a veces la segunda. Una vez más, vidas explícitas evitan tales ambigüedades.

En algunas situaciones simples, hay elision de por vida donde el compilador puede inferir vidas.

 3
Author: MichaelMoser,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-10 23:10:21

He encontrado otra gran explicación aquí: http://doc.rust-lang.org/0.12.0/guide-lifetimes.html#returning-references.

En general, solo es posible devolver referencias si son derivado de un parámetro del procedimiento. En ese caso, el puntero el resultado siempre tendrá la misma vida útil que uno de los parámetros; las vidas nombradas indican qué parámetro es.

 2
Author: corazza,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-02 19:41:00

Como recién llegado a Rust, mi entendimiento es que las vidas explícitas sirven para dos propósitos.

  1. Poner una anotación de vida explícita en una función restringe el tipo de código que puede aparecer dentro de esa función. Las vidas explícitas permiten que el compilador se asegure de que su programa está haciendo lo que pretendía.

  2. Si usted(el compilador) desea (n) comprobar si un fragmento de código es válido, usted (el compilador) no tendrá que buscar iterativamente dentro de cada función llamada. Basta con echar un vistazo a las anotaciones de las funciones que son llamadas directamente por esa pieza de código. Esto hace que su programa sea mucho más fácil de razonar para usted (el compilador), y hace que los tiempos de compilación sean manejables.

Sobre el punto 1., Considere el siguiente programa escrito en Python:

import pandas as pd
import numpy as np

def second_row(ar):
    return ar[0]

def work(second):
    df = pd.DataFrame(data=second)
    df.loc[0, 0] = 1

def main():
    # .. load data ..
    ar = np.array([[0, 0], [0, 0]])

    # .. do some work on second row ..
    second = second_row(ar)
    work(second)

    # .. much later ..
    print(repr(ar))

if __name__=="__main__":
    main()

Que imprimirá

array([[1, 0],
       [0, 0]])

Este tipo de comportamiento siempre me sorprende. Lo que está sucediendo es que df está compartiendo memoria con ar, así que cuando algunos del contenido de df cambia en work, ese cambio infecta ar también. Sin embargo, en algunos casos esto puede ser exactamente lo que desea, por razones de eficiencia de memoria (sin copia). El verdadero problema en este código es que la función second_row está devolviendo la primera fila en lugar de la segunda; buena suerte depurando eso.

Considere en su lugar un programa similar escrito en Rust:

#[derive(Debug)]
struct Array<'a, 'b>(&'a mut [i32], &'b mut [i32]);

impl<'a, 'b> Array<'a, 'b> {
    fn second_row(&mut self) -> &mut &'b mut [i32] {
        &mut self.0
    }
}

fn work(second: &mut [i32]) {
    second[0] = 1;
}

fn main() {
    // .. load data ..
    let ar1 = &mut [0, 0][..];
    let ar2 = &mut [0, 0][..];
    let mut ar = Array(ar1, ar2);

    // .. do some work on second row ..
    {
        let second = ar.second_row();
        work(second);
    }

    // .. much later ..
    println!("{:?}", ar);
}

Compilando esto, obtienes{[18]]}

error[E0308]: mismatched types
 --> src/main.rs:6:13
  |
6 |             &mut self.0
  |             ^^^^^^^^^^^ lifetime mismatch
  |
  = note: expected type `&mut &'b mut [i32]`
             found type `&mut &'a mut [i32]`
note: the lifetime 'b as defined on the impl at 4:5...
 --> src/main.rs:4:5
  |
4 |     impl<'a, 'b> Array<'a, 'b> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...does not necessarily outlive the lifetime 'a as defined on the impl at 4:5
 --> src/main.rs:4:5
  |
4 |     impl<'a, 'b> Array<'a, 'b> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^

De hecho, obtienes dos errores, también hay uno con los roles de 'a y 'b intercambiados. Mirando la anotación de second_row, encontramos que la salida debe ser &mut &'b mut [i32], es decir, la salida se supone que es una referencia a una referencia con vida 'b (la vida de la segunda fila de Array). Sin embargo, debido a que estamos devolviendo la primera fila (que tiene lifetime 'a), el compilador se queja de un desajuste de lifetime. En el lugar correcto. En el momento adecuado. La depuración es muy sencilla.

 1
Author: Jonas Dahlbæk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-11 13:16:15

La razón por la que su ejemplo no funciona es simplemente porque Rust solo tiene inferencia de vida útil y tipo local. Lo que está sugiriendo exige inferencia global. Siempre que tenga una referencia cuya vida útil no pueda ser eludida, debe ser anotada.

 0
Author: Klas. S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-28 21:51:10