Lifetimes avanzados en Rust: 'static, elision rules, lifetimes en structs y en impl

Los lifetimes garantizan que las referencias sean válidas durante todo el tiempo que se usan. El artículo introductorio explicó la sintaxis básica en funciones. Aquí vamos más allá: reglas de elisión que el compilador aplica automáticamente, anotaciones en structs e impl blocks, lifetime subtyping, el lifetime especial 'static y los Higher-Ranked Trait Bounds (for<'a>) para closures genéricos sobre lifetimes.

Reglas de elisión

El compilador aplica tres reglas automáticamente antes de pedir anotaciones explícitas:

  1. Cada parámetro de referencia recibe su propio lifetime.
  2. Si hay exactamente un parámetro de entrada con lifetime, ese lifetime se asigna a todos los de salida.
  3. Si hay un parámetro &self o &mut self, su lifetime se asigna a todos los de salida.
// Estas dos firmas son equivalentes gracias a la elisión:
fn primera_palabra(s: &str) -> &str { &s[..s.find(' ').unwrap_or(s.len())] }
fn primera_palabra_explicita<'a>(s: &'a str) -> &'a str { &s[..s.find(' ').unwrap_or(s.len())] }

// Aquí la elisión no puede resolver: hay dos parámetros, output ambiguo
// fn mas_largo(x: &str, y: &str) -> &str { ... } // ERROR
fn mas_largo<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Lifetimes en structs

// El struct no puede vivir más que la referencia que guarda
struct Fragmento<'a> {
    parte: &'a str,
}

impl<'a> Fragmento<'a> {
    fn nivel(&self) -> usize {
        self.parte.len()
    }

    // La anotación en impl deja claro que el retorno vive tanto como self
    fn anunciar_y_devolver<'b>(&'a self, aviso: &'b str) -> &'a str {
        println!("Atención: {aviso}");
        self.parte
    }
}

fn main() {
    let novela = String::from("Érase una vez...");
    let primera;
    {
        let frag = Fragmento { parte: novela.split('.').next().unwrap() };
        primera = frag.nivel();
    }
    println!("Longitud: {primera}");
}

Lifetime subtyping: 'a: 'b

La notación 'a: 'b significa que 'a vive al menos tanto como 'b. Útil cuando un lifetime debe sobrevivir a otro:

struct Cache<'a> {
    datos: &'a str,
}

// 'long: 'short garantiza que datos vive mientras exista la caché
fn crear_cache<'long: 'short, 'short>(datos: &'long str) -> Cache<'short> {
    Cache { datos }
}

fn main() {
    let texto = String::from("datos importantes");
    let cache = crear_cache(&texto);
    println!("{}", cache.datos);
}

El lifetime 'static

'static indica que la referencia vive durante todo el programa. Aparece en literales de cadena y en tipos que exige thread::spawn:

use std::thread;

// Los literales &str tienen lifetime 'static
let s: &'static str = "siempre vivo";

// thread::spawn exige que el closure sea 'static (no capture referencias cortas)
fn lanzar_hilo(mensaje: String) {
    thread::spawn(move || {
        println!("{mensaje}"); // mensaje es propiedad del closure: 'static OK
    });
}

// Truco para guardar datos estáticos
static CONFIGURACION: &str = "produccion";

fn main() {
    println!("{CONFIGURACION}");
    lanzar_hilo("hola".into());
}

Higher-Ranked Trait Bounds: for<'a>

Cuando un closure debe funcionar con referencias de cualquier lifetime (no uno específico), se usan HRTB:

// Esta función acepta un closure que trabaja con cualquier &str, sea cual sea su lifetime
fn aplicar_a_cadenas<F>(strings: &[&str], f: F) -> Vec<usize>
where
    F: for<'a> Fn(&'a str) -> usize,
{
    strings.iter().map(|s| f(s)).collect()
}

fn main() {
    let palabras = vec!["hola", "mundo", "rust"];
    let longitudes = aplicar_a_cadenas(&palabras, |s| s.len());
    println!("{longitudes:?}"); // [4, 5, 4]
}

Error típico: struct que sobrevive a su referencia

struct Contenedor<'a> {
    referencia: &'a String,
}

fn main() {
    let c;
    {
        let s = String::from("temporal");
        c = Contenedor { referencia: &s };
        // `s` se destruye aquí
    }
    println!("{}", c.referencia); // ERROR

    // error[E0597]: `s` does not live long enough
    // Se resuelve ampliando el scope de `s` o usando String en lugar de &String
}

Resumen

  • Las reglas de elisión cubren los casos más comunes; solo anotas explícitamente cuando son ambiguos.
  • Los structs con referencias necesitan parámetros de lifetime: el struct no puede vivir más que la referencia.
  • 'a: 'b (subtyping) expresa que un lifetime sobrevive a otro.
  • 'static indica que la referencia dura todo el programa; es lo que exige thread::spawn.
  • for<'a> (HRTB) permite closures genéricos sobre cualquier lifetime concreto.

COMPARTE ESTE ARTÍCULO

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