Lifetimes: anotando a validade das referências

1. O problema que lifetimes resolvem

Em Rust, toda referência tem um tempo de vida — o período durante o qual ela é válida. O sistema de lifetimes existe para garantir que nenhuma referência aponte para dados que já foram liberados da memória, prevenindo o temido dangling reference.

Considere o seguinte código que violaria a segurança de memória em outras linguagens:

fn retorna_referencia_pendente() -> &String {
    let s = String::from("exemplo");
    &s  // ERRO: s será destruída ao sair da função
}

O compilador Rust impede isso com a mensagem: "missing lifetime specifier". O borrow checker analisa os tempos de vida das referências e rejeita código onde uma referência sobrevive ao dado que ela referencia.

2. Sintaxe básica de anotação de lifetime

Lifetimes são anotados com apóstrofo seguido de um nome, geralmente 'a. A sintaxe básica em funções é:

fn maior_tamanho<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

O parâmetro 'a declara que todos os parâmetros de referência e o valor de retorno compartilham o mesmo tempo de vida. Isso significa que a referência retornada será válida enquanto ambos os parâmetros de entrada forem válidos.

Quando o compilador consegue inferir lifetimes automaticamente (regras de elisão), não precisamos anotar:

fn primeiro_elemento(v: &[i32]) -> Option<&i32> {
    v.first()
}

Mas em funções com múltiplas referências e retorno ambíguo, a anotação é obrigatória.

3. Lifetimes em funções com múltiplas referências

Quando uma função recebe múltiplas referências e retorna uma delas, precisamos especificar qual lifetime o retorno está vinculado:

fn escolher_maior<'a, 'b>(x: &'a str, y: &'b str) -> &'a str 
where 
    'b: 'a  // 'b vive pelo menos tanto quanto 'a
{
    if x.len() > y.len() { x } else { y }
}

A cláusula 'b: 'a significa "o lifetime 'b é maior ou igual a 'a". Isso garante que, mesmo retornando y (que tem lifetime 'b), a referência será válida dentro do escopo de 'a.

Exemplo prático:

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

fn main() {
    let s1 = String::from("Rust");
    let resultado;
    {
        let s2 = String::from("Linguagem");
        resultado = maior_string(&s1, &s2);
        println!("Maior: {}", resultado); // válido aqui
    }
    // println!("{}", resultado); // ERRO: s2 já foi dropado
}

4. Lifetimes em structs que armazenam referências

Structs que contêm referências precisam declarar lifetimes para garantir que não sobrevivam aos dados emprestados:

struct Livro<'a> {
    titulo: &'a str,
    autor: &'a str,
}

impl<'a> Livro<'a> {
    fn novo(titulo: &'a str, autor: &'a str) -> Self {
        Livro { titulo, autor }
    }

    fn descricao(&self) -> &str {
        self.titulo // lifetime elidido para &self
    }
}

fn main() {
    let titulo = String::from("1984");
    let autor = String::from("George Orwell");
    let livro = Livro::novo(&titulo, &autor);
    println!("{} por {}", livro.titulo, livro.autor);
} // livro é dropado antes de titulo e autor

A struct Livro<'a> não pode viver mais que as strings titulo e autor. O compilador garante isso analisando os escopos.

5. Regras de elisão de lifetimes (lifetime elision)

O Rust aplica três regras automáticas para evitar anotações verbosas:

  1. Cada parâmetro de referência ganha seu próprio lifetime: fn f(x: &T)fn f<'a>(x: &'a T)
  2. Se há exatamente um lifetime de entrada, ele é atribuído ao retorno: fn f(x: &T) -> &Ufn f<'a>(x: &'a T) -> &'a U
  3. Se há &self ou &mut self, seu lifetime é atribuído ao retorno: métodos de struct seguem essa regra

Quando as regras falham:

// ERRO: duas referências de entrada, retorno ambíguo
fn erro(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}
// Correção: fn erro<'a>(x: &'a str, y: &'a str) -> &'a str

Exemplos que funcionam sem anotação:

fn um_parametro(x: &i32) -> &i32 { x }           // regra 1+2
fn metodo(&self) -> &str { &self.campo }         // regra 3
fn sem_referencia(x: i32) -> i32 { x + 1 }       // sem lifetimes

6. Lifetimes e o modificador 'static

O lifetime 'static significa que a referência é válida por toda a execução do programa. O caso mais comum são strings literais:

let saudacao: &'static str = "Olá, mundo!";

Strings literais são armazenadas diretamente no binário e nunca são desalocadas. Outros usos incluem:

fn retorna_constante() -> &'static str {
    "Rust é seguro"
}

struct Configuracao {
    nome: &'static str,
}

⚠️ Cuidado: 'static não significa que o valor é imutável ou global — apenas que a referência dura para sempre. Não use 'static a menos que seja realmente necessário; ele pode criar restrições desnecessárias.

7. Boas práticas e armadilhas comuns

Prefira elisão sempre que possível — deixe o compilador inferir antes de adicionar anotações manuais.

Evite lifetimes muito amplos que amarram referências por mais tempo que o necessário:

// Ruim: força ambos os parâmetros a terem o mesmo lifetime
fn ruim<'a>(x: &'a str, y: &'a str) -> &'a str { ... }

// Melhor: lifetimes independentes quando o retorno não depende de ambos
fn melhor<'a>(x: &'a str, y: &str) -> &'a str { x }

Erro comum: conflitos entre lifetimes de parâmetros e retorno:

fn problema<'a>(x: &'a str, y: &'a str) -> &'a str {
    let local = String::from("temporário");
    // ERRO: não podemos retornar referência a variável local
    &local
}

Dica: lifetimes genéricos podem ser combinados com where para maior clareza:

fn complexo<'a, 'b, 'c>(a: &'a str, b: &'b str, c: &'c str) -> &'a str
where
    'b: 'a,
    'c: 'a,
{
    // lógica que retorna algo baseado em 'a
    a
}

Lifetimes são uma das características mais distintas de Rust. Eles permitem segurança de memória sem garbage collector, transferindo a verificação para tempo de compilação. Dominar esse conceito é essencial para escrever código Rust idiomático e seguro.

Referências