Rust lifetime

El lifetime en rust es básico para la gestión de las variables por parte del compilador: toda referencia tiene un tiempo de vida, que es el ámbito en que dicha referencia es válida. Si tratamos de usar una referencia cuando ya se acabó su lifetime, el compilador dará un error porque no podrá asegurar que son válidas cuando vayan a usarse.

En el vídeo sobre el tema de la serie Crust of Rust, Lifetime Annotations, se usa el siguiente ejemplo:

  struct StrSplit<'a> {
    remainder: &'a str,
    delimiter: &'a str,
}

Aquí, ambos miembros del struct son préstamos de alguna zona de memoria donde reside un str. Como no son datos que pertenezcan a él, el compilador necesita asegurarse que dichas zonas de memoria no sean liberadas mientras la instancia de StrSplit es válida. Dicho de otra forma, necesita saber que ambos campos apuntan a zonas de memoria válidas en todo momento. Como dice el libro:

[…] lifetimes ensure that references are valid as long as we need them to be.

Al marcar con <'a>, lo que estamos diciendo es que dichos &str viven el mismo tiempo que ella (que <'a>). El compilador comprobará que en efecto esto es cierto y que cualquier referencia que se pase a StrSplit::new(...) dura al menos tanto tiempo como la instancia del struct.

‘static

‘static es el tiempo de vida que dura hasta que el programa finaliza. Por eso, donde se necesita pasar una referencia a un lifetime, siempre podemos pasar ‘static. Esto es una relación de subtipo, donde el que dura más tiempo siempre se puede usar donde se requiere un tiempo de vida de menor duración, pero obviamente no al revés.

<’_>

Podemos tener tiempos de vida anónimos. En ellos dejamos que el compilador suponga/infiera cual es el /lifetime/. Solamente son válidos cuando la suposición es unívoca y el compilador no necesita elegir entre varias posibilidades.

Si implementamos alguna función podemos tener

impl StrSplit<'_> {
    fn new(&str a, &str b) -> Self {
        StrSplit {
            remainder: a,
            delimeter: b
    }
}

El compilador lo acepta porque puede inferir que el lifetime de ambos parámetros es el mismo y es el que acabará pasando /StrSplit/. Lo siguiente es equivalente pero bastante más verboso[fn:1]:

  impl<'a> StrSplit<'a> {
    pub fn new(haystack: &'a str, delimiter: &'a str) -> Self {
        Self {
            remainder: haystack,
            delimiter,
        }
    }
}

Si en este caso usamos lifetime anónimo el compilador nos advierte de tiempos de vida distintos que pueden o no ser lo sufientemente largos. El error es muy descriptivo (limpio in poco):

error: lifetime may not live long enough
       pub fn new(haystack: &str, delimiter: &str) -> Self {
                            -                         ---- return type is StrSplit<'2>
                            |
                            let's call the lifetime of this reference `'1`
 /         Self {
 |             remainder: haystack,
 |             delimiter,
 |         }
 |_________^ associated function was supposed to return data with lifetime `'2` but it is returning data with lifetime `'1`

error: lifetime may not live long enough
       pub fn new(haystack: &str, delimiter: &str) -> Self {
                                             -        ---- return type is StrSplit<'2>
                                             |
                                             let's call the lifetime of this reference `'3`
 /         Self {
 |             remainder: haystack,
 |             delimiter,
 |         }
 |_________^ associated function was supposed to return data with lifetime `'2` but it is returning data with lifetime `'3`

Es recomendable para simplificar, siempre que podamos, usar este tiempo de vida anónimo.

Múltiples lifetimes

Normalmente no necesario.

Para cuando necesitamos tener múltiples referencias distintas y el tiempo de vida del contenedor no está relacionado más que con un de ellas.

En el vídeo se nos presenta el siguiente código:

fn until_char<'s>(s: &'s str, c: &str) -> &'s str {
    StrSplit::new(s, &format!("{}", c))
        .next()
        .expect("StrSlit always gives at least one result")
}

El cual no puede compilar debido a que el tiempo de vida del delimitador (&format("{c}")) es menor que el tiempo de vida que necesitamos. Su vida es local a la propia función obviamente, pero necesitamos que lo que devolvemos dure más. Pero el lifetime es el mismo para remainder y delimiter en nuestra definición de StrSplit, por lo que necesitamos que no sean el mismo, porque realmente con que esté relacionado con el tiempo del haystack es suficiente. Así, con dos tiempos distintos lo solucionamos.

  pub struct StrSplit<'a, 'b> {
    remainder: Option<&'a str>,
    delimiter: &'b str,
}

impl<'a, 'b> StrSplit<'a, 'b> {
    pub fn new(haystack: &'a str, delimiter: &'b str) -> Self {
        Self {
            remainder: Some(haystack),
            delimiter,
        }
    }
}

impl<'a> Iterator for StrSplit<'a, '_> {
    type Item = &'a str;
    ...
}

En la última implementación ni siquiera necesitamos definir un tiempo ‘b, y con el anónimo nos sobra, porque lo vamos a acabar descartando.

Referencias