Lifetimes en Rust: decirle al compilador cuánto viven las referencias

El compilador de Rust necesita saber cuánto tiempo vive cada referencia para garantizar que nunca apuntas a memoria liberada. La mayoría de las veces puede inferirlo solo con las lifetime elision rules. Cuando no puede, necesitas anotar los lifetimes explícitamente con la sintaxis 'a.

El problema: referencias con duraciones distintas

fn mayor_string(s1: &str, s2: &str) -> &str { // ERROR
    if s1.len() > s2.len() { s1 } else { s2 }
}
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:43
  |
1 | fn mayor_string(s1: &str, s2: &str) -> &str {
  |                     ----      ----     ^ expected named lifetime parameter

El compilador no sabe si la referencia retornada viene de s1 o de s2, y por tanto no sabe cuánto tiempo debe vivir. Necesitas anotarlo.

Sintaxis de lifetime annotations

fn mayor_string<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() { s1 } else { s2 }
}

fn main() {
    let s1 = String::from("larga");
    let resultado;
    {
        let s2 = String::from("xyz");
        resultado = mayor_string(&s1, &s2);
        println!("{}", resultado); // OK: s2 vive hasta aquí
    }
    // println!("{}", resultado); // ERROR: s2 ya no existe
}

La anotación 'a dice: "la referencia retornada vivirá como máximo lo que viva la más corta de las dos entradas". El compilador usa esto para verificar que no hay dangling references.

Lifetimes en structs

struct Extracto<'a> {
    contenido: &'a str,
}

impl<'a> Extracto<'a> {
    fn mostrar(&self) {
        println!("{}", self.contenido);
    }
}

fn main() {
    let novela = String::from("Era una noche oscura...");
    let primera_frase = novela.split('.').next().expect("al menos una frase");
    let e = Extracto { contenido: primera_frase };
    e.mostrar();
    // e no puede vivir más que novela
}

Lifetime elision rules

El compilador aplica estas tres reglas y si puede determinar todos los lifetimes de salida, no necesitas anotarlos:

  1. Cada referencia de entrada obtiene su propio lifetime: fn f(x: &T, y: &U) ? fn f<'a, 'b>(x: &'a T, y: &'b U).
  2. Si hay un único parámetro de entrada, su lifetime se aplica a todas las salidas.
  3. Si hay un parámetro &self o &mut self, su lifetime se aplica a todas las salidas.
// No necesita anotación: se aplica la regla 2
fn primera_palabra(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

// No necesita anotación: se aplica la regla 3
impl Extracto<'_> {
    fn contenido(&self) -> &str {
        self.contenido
    }
}

El lifetime estático: 'static

// Los literales de string tienen lifetime 'static: viven todo el programa
let s: &'static str = "Hola, mundo";

// También lo tienen las constantes globales
static SALUDO: &str = "Hola";

Lifetimes, generics y trait bounds juntos

use std::fmt::Display;

fn mayor_con_anuncio<'a, T: Display>(s1: &'a str, s2: &'a str, anuncio: T) -> &'a str {
    println!("Anuncio: {}", anuncio);
    if s1.len() > s2.len() { s1 } else { s2 }
}

Resumen

  • Los lifetimes le dicen al compilador cuánto deben vivir las referencias.
  • Sintaxis: 'a en la firma de función, struct o impl.
  • 'a en retorno significa "vive al menos tanto como la entrada con el mismo lifetime".
  • Las elision rules infieren lifetimes en los casos más comunes: no necesitas anotarlos.
  • 'static: el lifetime del programa completo.

El siguiente artículo cubre los traits esenciales de la stdlib: Display, Debug, From, Into, PartialEq, Ord y Default, que encontrarás en casi cualquier código Rust.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP