rust-lifetimes-anotaciones-tiempo-vida

Lifetimes en Rust: entender y anotar tiempos de vida

  • 5 min

Un lifetime es el intervalo durante el que una referencia es válida; sus anotaciones expresan relaciones entre las referencias cuando el compilador no puede elidirlas.

Hasta ahora, el Borrow Checker ha sido nuestro ángel de la guarda. Se aseguraba de que ninguna referencia viviera más tiempo que el dato al que apunta.

Repasemos el caso clásico de una Referencia Colgante (Dangling Reference), que Rust impide:

fn main() {
    let r;   
                      
    {                  
        let x = 5;     
        r = &x;       
    }                   
                       
    println!("r: {}", r); 
}
Copied!

El compilador mira este código y dice: “Error: x vive menos tiempo que r. Si permito esto, r apuntará a memoria vacía cuando se cierre la llave interna.”

En este caso, los “ámbitos” (scopes) son obvios visualmente. Pero, ¿qué pasa cuando pasamos referencias a funciones?

El problema de la ambigüedad

Imagina una función que compara dos cadenas de texto y devuelve la más larga.

fn mas_larga(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
Copied!

Si intentas compilar esto, Rust te dará un error: missing lifetime specifier.

¿Por qué?

Analicémoslo desde el punto de vista del compilador:

  1. La función recibe dos referencias: x e y.
  2. Devuelve una referencia.
  3. La firma no especifica si la referencia devuelta está ligada a x, a y o a ambos.
  4. Rust necesita esa relación en el contrato de la función para comprobar cada llamada sin depender de su implementación interna.

Para esos casos usamos las Anotaciones de Lifetime.

La sintaxis 'a

Un Lifetime es un tipo de genérico. Igual que <T> generaliza sobre tipos, <'a> generaliza sobre tiempos de vida. La sintaxis usa un apóstrofe seguido de un nombre (normalmente a, b, c…).

Vamos a arreglar la función anterior:

// Leemos: "La función mas_larga vive durante un tiempo 'a.
// Recibe dos parámetros que viven AL MENOS tanto como 'a.
// Devuelve una referencia que vivirá AL MENOS tanto como 'a."
fn mas_larga<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
Copied!

¿Qué significa esto realmente?

Esta es la clave para entenderlo: Las anotaciones NO cambian el tiempo de vida real de las variables. No hacen que vivan más o menos.

Simplemente le dicen al compilador cómo se relacionan entre sí. En el ejemplo anterior, le estamos diciendo a Rust:

“La referencia de retorno será válida siempre y cuando ambas referencias de entrada sigan siendo válidas.”

En la práctica, 'a será igual al más pequeño de los tiempos de vida de x e y.

fn main() {
    let string1 = String::from("larga");
    let resultado;

    {
        let string2 = String::from("xyz");
        // Aquí, 'a es el scope de string2 (el más pequeño)
        resultado = mas_larga(string1.as_str(), string2.as_str());
    }

    // ❌ Error: `resultado` ya no es válido porque string2 murió,
    // y el contrato decía que el resultado vive lo mismo que el menor de los inputs.
    // println!("La más larga es {}", resultado);
}
Copied!

Gracias a la anotación 'a, Rust detecta el error correctamente.

Structs con referencias

Hasta ahora, nuestros Structs siempre eran dueños de sus datos (String, Vec<i32>). Pero, ¿y si queremos que un Struct guarde una referencia a un dato que pertenece a otra persona?

Rust necesita saber que el Struct no vivirá más tiempo que el dato al que apunta.

// Este struct NO es dueño del texto. Solo guarda una referencia (slice).
struct Extracto<'a> {
    parte: &'a str,
}

fn main() {
    let novela = String::from("En un lugar de la Mancha...");
    let primera_frase = novela.split('.').next().expect("No hay puntos");

    // Creamos el struct
    let i = Extracto {
        parte: primera_frase,
    };

    // Esto funciona porque 'novela' vive más que 'i'
}
Copied!

Si no pusiéramos <'a>, Rust no nos dejaría compilar el struct. Nos estaría protegiendo de crear un struct que, al usarlo más tarde, apuntara a memoria liberada.

Elisión de lifetimes

Seguro que estás pensando: “Espera, hemos escrito funciones que devolvían &str antes y no pusimos 'a. ¿Por qué ahora sí?”

Rust tiene unas reglas históricas llamadas Reglas de Elisión. El compilador intenta inferir los lifetimes en casos comunes para ahorrarte escribir.

Las 3 reglas que aplica el compilador automáticamente:

  1. Cada parámetro referencia tiene su propio lifetime (fn foo(x: &'a i32, y: &'b i32)).
  2. Si hay un solo parámetro de entrada, ese lifetime se asigna a todas las salidas (fn foo(x: &'a i32) -> &'a i32).
  3. Si es un método (&self), el lifetime de self se asigna a todas las salidas.

Solo cuando estas reglas no aclaran la ambigüedad (como en mas_larga, donde hay dos inputs y un output), tienes que escribirlo a mano.

El lifetime estático: 'static

Existe un lifetime especial reservado: 'static. Significa que la referencia puede vivir durante toda la duración del programa.

Todos los literales de cadena son 'static porque están guardados directamente en el binario del programa.

let s: &'static str = "Tengo un lifetime infinito";
Copied!

A veces verás 'static como restricción en genéricos (Trait Bounds), indicando que el tipo no puede contener referencias temporales.