Tiempos de vida (lifetimes)

El tiempo de vida asociado a todos los valores y variables indica "cuánto vive una variable". Solo es necesario pensar en ellos cuando se trabaja con referencias. Esto se debe a que las referencias no pueden vivir más tiempo que el propio objeto al que referencian. Por ejemplo, esta función no compila:

fn returns_reference() -> &str {
    let my_string = String::from("I am a string");
    &my_string // ⚠️
}

fn main() {}

El problema es que my_string solo vive dentro de la propia función, pero la función intenta devolver una referencia &my_string y esta no podrá existir cuando se libere my_string al terminar de ejecutarse la función. Por eso el compilador falla.

Este otro código tampoco funciona:

fn returns_str() -> &str {
    let my_string = String::from("I am a string");
    "I am a str" // ⚠️
}

fn main() {
    let my_str = returns_str();
    println!("{}", my_str);
}

Aunque por poco. El compilador indica:

error[E0106]: missing lifetime specifier
 --> src\main.rs:6:21
  |
6 | fn returns_str() -> &str {
  |                     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
6 | fn returns_str() -> &'static str {
  |                     ^^^^^^^^

El mensaje missing lifetieme specifier significa que tenemos que añadir ' con un tiempo de vida.

Después indica contains a borrowed value, but there is no value for it to be borrowed from. Esto signfiica que I am a str no se obtiene de ningún sitio. Además, indica consider using the 'static lifetime escribiendo &'static str. Por lo que el compilador piensa que se debe indicar que la variable es de tipo cadena de caracteres literal (como es el caso).

Con la modificación, lo siguiente funciona:

fn returns_str() -> &'static str {
    let my_string = String::from("I am a string");
    "I am a str"
}

fn main() {
    let my_str = returns_str();
    println!("{}", my_str);
}

Que funcione, se debe a que se ha indicado al compilador que esta función devuelve un &str con un tiempo de vida estático. Si se quisiera evolver my_string solo podría hacerse como String, no como referencia. El fallo del paso por referencia del primer ejemplo de este apartado, se debe a que la propiedad no se traspasaría al código que llamara a la función y en la siguiente línea se eliminaría el valor de my_string. Rust evita que exista una referencia cuyo tiempo de vida sea mayor que el del valor al que referencia.

Ahora fn returns_str() -> &'static str le dice a Rust que no debe preocuparse, se retorna una cadena de caracteres literal. La cadena de caracteres literal dura toda la ejecución del programa. Se observa que se asemeja a los genéricos. Cuando se indica al compilador algo como <T: Display>, se le está diciendo que solo se va a usar este código con tipos Display. Los tiempos de vida son similares: no se está cambiando nada en las propias variables, solo se está indicando al compilador cuales serán los tiempos de vida de cada variable de entrada y de salida.

Lógicamente, 'static no es el único tiempo de vida posible. Cada variable tiene su tiempo de vida, aunque normalmente no es necesario indicarlo en el código. El compilador es inteligente y puede deducirlo por sí mismo. Solo es necesario expresarlo en el código cuando el compilador no puede hacerlo.

A continuación se muestra un ejemplo de otro tiempo de vida. Si se quiere crear un struct Ciudad y pasarle un &str para el nombre (por ejemplo, para que su rendimiento sea mejor que con un String). Se puede intentar así (este código no funciona):

#[derive(Debug)]
struct Ciudad {
    name: &str, // ⚠️
    date_founded: u32,
}

fn main() {
    let mi_ciudad = Ciudad {
        name: "Ichinomiya",
        date_founded: 1921,
    };
}

El compilador indica:

error[E0106]: missing lifetime specifier
 --> src\main.rs:3:11
  |
3 |     name: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
2 | struct Ciudad<'a> {
3 |     name: &'a str,
  |

Rust necesita un tiempo de vida para &str porque &str es una referencia. ¿Qué sucede cuando el valor al que apunta name se libera? No sería seguro usar este código.

¿Qué pasa si se pone 'static? ¿funcionará como en los casos anteriores? Si se prueba:

#[derive(Debug)]
struct Ciudad {
    name: &'static str, 
    date_founded: u32,
}

fn main() {
    let mi_Ciudad = Ciudad {
        name: "Ichinomiya",
        date_founded: 1921,
    };

println!("{} se fundó en {}", mi_Ciudad.name, mi_Ciudad.date_founded);

}

En este caso funciona. Sin embargo, solo se le pueden pasar cadenas de caracteres literales, no referencias a otro tipo de valores. Por eso, este otro código no funciona:

#[derive(Debug)]
struct Ciudad {
    name: &'static str, // debe existir el valor durante todo el programa
    date_founded: u32,
}

fn main() {
    let Ciudad_names = vec!["Ichinomiya".to_string(), "Kurume".to_string()]; // Ciudad_names no vive durante todo el programa

    let my_Ciudad = Ciudad {
        name: &Ciudad_names[0], // ⚠️ Es un &str, pero no &'static str. Es una referencia a un valor interno de Ciudad_names
        date_founded: 1921,
    };

    println!("{} se fundó en {}", my_Ciudad.name, my_Ciudad.date_founded);
}

El compilador dice:

error[E0597]: `Ciudad_names` does not live long enough
  --> src\main.rs:12:16
   |
12 |         name: &Ciudad_names[0],
   |                ^^^^^^^^^^
   |                |
   |                borrowed value does not live long enough
   |                requires that `Ciudad_names` is borrowed for `'static`
...
18 | }
   | - `Ciudad_names` dropped here while still borrowed

Este ejemplo es importante entenderlo, ya que la referencia que se pasa sí que vive lo suficiente. Pero lo que se ha indicado en el código es que solo se le va a pasar 'static str' y ese es el problema.

Así que se va a intentar lo que el compilador sugería antes: escribir struct Ciudad<'a> y name: &'a str. Que significa que la referencia a name solo existe mientras exista el valor struct Ciudad.

#[derive(Debug)]
struct Ciudad<'a> { // City has lifetime 'a
    name: &'a str, // and name also has lifetime 'a.
    date_founded: u32,
}

fn main() {
    let ciudad_names = vec!["Ichinomiya".to_string(), "Kurume".to_string()];

    let my_Ciudad = Ciudad {
        name: &ciudad_names[0],
        date_founded: 1921,
    };

    println!("{} se fundó en {}", my_Ciudad.name, my_Ciudad.date_founded);
}

Es necesario recordar que se puede escribir cualquier cosa en lugar de 'a. Vuelve a ser similar a los genéricos en los que se escribe T y U, pero se puede escribir cualquier palabra.

#[derive(Debug)]
struct City<'city> { // El tiempo de vida se llama ahora 'city
    name: &'city str, // y name vive solo lo que 'city
    date_founded: u32,
}

fn main() {}

En todo caso, se suelen usar por convención 'a, 'b, 'c, etc. En caso de usar otros nombres, es conveniente aprovechar para que tengan sentido para los humanos.

Se presenta de nuevo una comparación con los rasgos y genéricos. Por ejemplo:

use std::fmt::Display;

fn prints<T: Display>(input: T) {
    println!("T is {}", input);
}

fn main() {}

T:Display significa que solo se puede usar un valor si este implementa Display. No significa que doy Display a T.

Lo mismo sucede con los tiempos de vida. Si se escribe:

#[derive(Debug)]
struct City<'a> {
    name: &'a str,
    date_founded: u32,
}

fn main() {}

Significa que solo se permiten valores de referencia para name que duren al menos lo mismo que el struct de City que se está creando. No significa que el valor asignado a name pase a dura lo mismo que el struct de City creado.

Ahora se puede entender <'_> que se apareció anteriormente. Se denomina "tiempo de vida anónimo" y es un indicador de que se están usando referencias. Rust lo sugiere cuando se implementan struct. Por ejemplo: este es un código que no funciona:

    // ⚠️
struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

fn main() {}

Se ha hecho todo lo necesario para que la referencia en el name requira tener el tiempo de vida del struct como mínimo. Sin embargo, Rust se queja de la parte de la implementación.

error[E0726]: implicit elided lifetime not allowed here
 --> src\main.rs:6:6
  |
6 | impl Adventurer {
  |      ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`

Pide que se añada una tiempo de vida anónimo para que quede claro en la implementación que existe una referencia en este código que se está usando. Basta con atender a la sugerencia:

struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer<'_> {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

fn main() {}

Este tiempo de vida anónimo simplifica la forma general que tendría que haber tenido en este caso impl<'a> Adventurer<'a>.

Los tiempos de vida son uno de los temas que puede ser más difícil en Rust. A continuación se muestran algunas sugerencias para que sirvan de ayuda:

  • Se pueden evitar las referencias pasando clones y con objetos copy, etc.
  • La mayor parte de las veces en las que el compilador necesita un tiempo de vida, simplemente habrá que escribir 'a en un par de sitios y funcionará.
  • Se puede ir aprendiendo este tema en pequeñas dosis. Se puede escribir el código con valores propietarios, sin referencias, luego convertir uno de ellos en referencia. El compilador empezará a quejarse y a dar sugerencias. Si se complica mucho, se puede deshacer el cambio e intentarlo más tarde.

Se presenta aquí un ejemplo que ya contiene una referencia y no indica tiempo de vida alguno. Se seguirán las indicaciones para subsanarlo.

// ⚠️
struct Adventurer {
    name: &str,
    hit_points: u32,
}

impl Adventurer {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

impl std::fmt::Display for Adventurer {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{} has {} hit points.", self.name, self.hit_points)
        }
}

fn main() {}

La primera queja es:

error[E0106]: missing lifetime specifier
 --> src\main.rs:2:11
  |
2 |     name: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 | struct Adventurer<'a> {
2 |     name: &'a str,
  |

Sugiere qué es lo que hay que hacer:

// ⚠️
struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

impl std::fmt::Display for Adventurer {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{} has {} hit points.", self.name, self.hit_points)
        }
}

fn main() {}

Ahora no tiene problema con esa parte de código, pero se queja de otra parte:

error[E0726]: implicit elided lifetime not allowed here
 --> src\main.rs:6:6
  |
6 | impl Adventurer {
  |      ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`

error[E0726]: implicit elided lifetime not allowed here
  --> src\main.rs:12:28
   |
12 | impl std::fmt::Display for Adventurer {
   |                            ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`

Así que se sigue la sugerencia y se modifica como pide:

struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer<'_> {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

impl std::fmt::Display for Adventurer<'_> {

        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{} has {} hit points.", self.name, self.hit_points)
        }
}

fn main() {
    let mut billy = Adventurer {
        name: "Billy",
        hit_points: 100_000,
    };
    println!("{}", billy);
    billy.take_damage();
}

Esto da como salida:

Billy has 100000 hit points.
Billy has 99980 hit points left!

Así se puede observar que los tiempos de vida en muchas ocasiones sirven para que el compilador se asegure de que no se está cometiendo un error. Normalmente es lo suficientemente inteligente para determinar qué tiempo de vida tiene cada valor y solo "pregunta" cuando no lo puede saber con seguridad.