En esta página

Lifetimes: anotando la vida de las referencias

14 min lectura TextoCap. 2 — Ownership y borrowing

¿Por qué existen las lifetimes?

Las lifetimes (tiempos de vida) son la manera en que Rust rastrea cuánto tiempo viven las referencias. No son un concepto que existe en tiempo de ejecución — son puramente información para el compilador.

El borrow checker usa lifetimes para garantizar que ninguna referencia viva más que el dato al que apunta. Sin esta verificación, podríamos tener punteros colgantes.

El problema que resuelven

// ¿Cuál es el lifetime del valor de retorno?
fn mas_largo(x: &str, y: &str) -> &str {
    if x.len() >= y.len() { x } else { y }
}

Esta función no compila porque el compilador no sabe si el valor de retorno está relacionado con x o con y. Si x y y tienen lifetimes distintos, el valor de retorno podría ser una referencia a algo que ya se liberó.

Sintaxis de anotaciones de lifetime

Las anotaciones de lifetime comienzan con ' seguido de un nombre (típicamente una sola letra minúscula):

&i32        // una referencia
&'a i32     // una referencia con lifetime 'a explícito
&'a mut i32 // una referencia mutable con lifetime 'a explícito

Las anotaciones no cambian cuánto viven las referencias — solo describen la relación entre los lifetimes de múltiples referencias.

Lifetimes en funciones

// 'a es el lifetime más corto entre x e y
// El valor de retorno vive al menos 'a
fn mas_largo<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() >= y.len() { x } else { y }
}

fn main() {
    let cadena1 = String::from("cadena larga es larga");
    
    // Caso 1: Ambas viven el mismo tiempo
    let cadena2 = String::from("xyz");
    let resultado = mas_largo(cadena1.as_str(), cadena2.as_str());
    println!("La más larga: {resultado}");
    
    // Caso 2: Una vive menos que la otra
    let resultado2;
    {
        let cadena3 = String::from("otra cadena");
        // resultado2 = mas_largo(&cadena1, &cadena3);
        // println!("{resultado2}"); // Error si cadena3 ya se dropó
        
        // Correcto: usar el resultado dentro del scope de cadena3
        let r = mas_largo(&cadena1, &cadena3);
        println!("Resultado en scope: {r}");
    }
}

Reglas de elision de lifetimes

En la práctica, no siempre necesitas escribir lifetimes manualmente. El compilador aplica tres reglas de elision (elision = omisión) para inferirlas automáticamente:

Regla 1: Cada parámetro de referencia recibe su propio lifetime.

// Lo que escribes:
fn primera(s: &str) -> &str

// Lo que el compilador infiere:
fn primera<'a>(s: &'a str) -> &'a str

Regla 2: Si hay exactamente un parámetro de referencia de entrada, su lifetime se asigna a todos los outputs de referencia.

// Lo que escribes:
fn primera_palabra(s: &str) -> &str

// Lo que infiere el compilador:
fn primera_palabra<'a>(s: &'a str) -> &'a str

Regla 3: Si uno de los parámetros es &self o &mut self, su lifetime se asigna a todos los outputs de referencia.

struct Texto { contenido: String }

impl Texto {
    // Lo que escribes:
    fn obtener_slice(&self) -> &str
    
    // Lo que infiere el compilador:
    // fn obtener_slice<'a>(&'a self) -> &'a str
}

Structs con lifetimes

Cuando un struct contiene referencias, necesita anotaciones de lifetime:

// El struct no puede outlive la referencia que contiene
struct Fragmento<'a> {
    texto: &'a str,
    inicio: usize,
    fin: usize,
}

impl<'a> Fragmento<'a> {
    fn nuevo(texto: &'a str, inicio: usize, fin: usize) -> Self {
        Fragmento { texto, inicio, fin }
    }
    
    fn contenido(&self) -> &str {
        &self.texto[self.inicio..self.fin]
    }
    
    fn longitud(&self) -> usize {
        self.fin - self.inicio
    }
}

fn main() {
    let texto = String::from("Rust es un lenguaje de sistemas moderno");
    
    let fragmento = Fragmento::nuevo(&texto, 0, 4);
    println!("Fragmento: '{}'", fragmento.contenido()); // "Rust"
    println!("Longitud: {}", fragmento.longitud());
    
    // Fragmento no puede outlive texto
    // drop(texto); // Error si usamos fragmento después
    println!("Texto completo: {texto}");
}

Lifetime bounds: restricciones de lifetime

Puedes añadir restricciones de lifetime a tipos genéricos:

use std::fmt::Display;

// T debe vivir al menos 'a
fn mostrar_referencia<'a, T>(valor: &'a T)
where
    T: Display + 'a,
{
    println!("Valor: {valor}");
}

// Una referencia a T debe vivir 'a, y T debe implementar Display
fn encontrar_mas_largo<'a, T>(
    lista: &'a [T],
    predicado: impl Fn(&T) -> bool,
) -> Option<&'a T>
where
    T: Display,
{
    lista.iter().find(|item| predicado(item))
}

fn main() {
    let numeros = vec![1, 5, 2, 8, 3, 7];
    
    if let Some(encontrado) = encontrar_mas_largo(&numeros, |&n| n > 5) {
        println!("Encontrado: {encontrado}");
    }
    
    let palabras = vec!["hola", "mundo", "rust"];
    mostrar_referencia(&palabras[0]);
}

La lifetime `'static`

'static es una lifetime especial que significa "vive por toda la duración del programa":

fn main() {
    // String literals son 'static — viven en el binario
    let saludo: &'static str = "Hola, mundo";
    
    // Las constantes también son 'static
    static CONSTANTE: &str = "soy estática";
    
    println!("{saludo}");
    println!("{CONSTANTE}");
}

// Esta función puede retornar una referencia 'static
fn obtener_mensaje(codigo: u32) -> &'static str {
    match codigo {
        200 => "OK",
        404 => "No encontrado",
        500 => "Error interno",
        _   => "Código desconocido",
    }
}

'static también aparece en los bounds de trait para objetos trait: Box<dyn Error + 'static> o Box<dyn Error + Send + 'static>. Esto indica que el tipo de error no contiene referencias con lifetimes menores a 'static.

Lifetimes en la práctica

En código Rust real, raramente necesitas escribir lifetimes manualmente porque:

  1. Las reglas de elision cubren los casos más comunes
  2. Los structs que poseen sus datos (usando String en lugar de &str, Vec en lugar de &[]) no necesitan anotaciones
// Sin lifetimes — posee sus datos
struct Usuario {
    nombre: String,  // String, no &str
    edad: u32,
}

// Con lifetimes — referencia a datos externos
struct VistaUsuario<'a> {
    nombre: &'a str,  // &str, necesita lifetime
    edad: u32,
}

La regla general: prefiere tipos que poseen sus datos en los structs. Usa referencias solo cuando el rendimiento lo requiera y estés dispuesto a gestionar los lifetimes.


Con ownership, borrowing y lifetimes comprendidos, tienes el conocimiento del sistema de tipos único de Rust. La próxima lección introduce los structs — la forma principal de crear tipos de datos personalizados.

Las lifetimes son inferidas en la mayoría de casos
Las reglas de elision de lifetimes permiten omitir anotaciones en los casos más comunes: funciones con un solo parámetro de referencia, métodos con &self. Solo necesitas anotar explícitamente cuando el compilador no puede inferirlas.
'static: la lifetime especial
La lifetime 'static significa que la referencia vive por toda la duración del programa. Los string literals (&str) son siempre 'static porque están embebidos en el binario. Los mensajes de error de Box<dyn Error + 'static> también usan esta lifetime.
rust
// Sin anotación: el compilador no sabe cuánto vive el retorno
// fn mas_largo(x: &str, y: &str) -> &str { ... } // Error

// Con anotación de lifetime 'a
fn mas_largo<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() >= y.len() { x } else { y }
}

// Struct que contiene una referencia necesita lifetime
struct Extracto<'a> {
    parte: &'a str,
}

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

fn main() {
    let cadena1 = String::from("cadena larga");
    let resultado;
    {
        let cadena2 = String::from("xyz");
        resultado = mas_largo(&cadena1, &cadena2);
        println!("La más larga es: {resultado}");
    }

    // Struct con referencia — el texto debe vivir más que el struct
    let novela = String::from("Llámame Ishmael. Hace algunos años...");
    let primera_frase;
    {
        let i = novela.find('.').unwrap_or(novela.len());
        primera_frase = Extracto { parte: &novela[..i] };
        primera_frase.mostrar();
    }
}