Lifetimes em Rust: Guia Avançado | Rust Brasil

Lifetimes em profundidade: anotações, elision rules, structs com lifetime, HRTB e como o borrow checker funciona.

Lifetimes são um dos conceitos mais distintos do Rust. Enquanto outras linguagens lidam com a validade de referências em tempo de execução (ou simplesmente ignoram o problema), o Rust exige que toda referência tenha um lifetime — uma anotação que diz ao compilador por quanto tempo aquela referência é válida. Neste artigo, vamos explorar lifetimes em profundidade: da sintaxe básica até Higher-Rank Trait Bounds.

O Que São Lifetimes e Por Que Existem?

Um lifetime é a região do código durante a qual uma referência é válida. O compilador Rust (especificamente, o borrow checker) usa lifetimes para garantir que nenhuma referência aponte para dados que já foram desalocados — o famoso dangling reference.

Considere este exemplo que não compila:

fn main() {
    let r;
    {
        let x = 5;
        r = &x; // x será destruído ao final deste bloco
    }
    // println!("{}", r); // ERRO: x não vive o suficiente
}

O compilador detecta que x vive apenas dentro do bloco interno, mas r tenta usá-lo fora. Sem lifetimes, isso seria um bug em tempo de execução.

O Diagrama Mental

    'a (lifetime de r)
    |-------------------------------------|
    |   'b (lifetime de x)                |
    |   |------------|                    |
    |   | x = 5      |                    |
    |   | r = &x     |                    |
    |   |------------|  <- x é destruído  |
    |                                     |
    |   println!("{}", r) <- r é inválido |
    |-------------------------------------|

O borrow checker verifica que 'b (o lifetime de x) é menor que 'a (o lifetime de r) e, portanto, a referência não é segura.

Sintaxe de Lifetimes: O 'a

Quando o compilador não consegue inferir lifetimes automaticamente, você precisa anotá-los. A sintaxe usa um apóstrofo seguido de um nome (geralmente uma letra minúscula):

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

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

    {
        let string2 = String::from("xyz");
        resultado = maior(string1.as_str(), string2.as_str());
        println!("O maior é: {}", resultado);
    }
    // Se tentássemos usar 'resultado' aqui, não compilaria
    // porque string2 já saiu do escopo
}

A anotação 'a diz: “o retorno desta função viverá pelo menos tanto quanto o menor dos lifetimes de x e y”. Isso é crucial — o compilador usa essa informação para garantir que o resultado não sobreviva às suas fontes.

Lifetimes Não Mudam o Tempo de Vida Real

Um equívoco comum: anotações de lifetime não alteram quanto tempo os valores vivem. Elas apenas descrevem as relações entre os lifetimes de diferentes referências para que o compilador possa verificar a segurança.

Regras de Elisão de Lifetimes

Nem toda função precisa de anotações explícitas. O Rust tem três regras de elisão que inferem lifetimes automaticamente:

Regra 1 — Cada parâmetro de referência recebe seu próprio lifetime:

// O que você escreve:
fn primeiro(s: &str) -> &str { ... }

// O que o compilador entende:
fn primeiro<'a>(s: &'a str) -> &'a str { ... }

Regra 2 — Se há exatamente um parâmetro de referência de entrada, seu lifetime é atribuído a todas as referências de saída:

// Funciona sem anotação porque só há um parâmetro de referência
fn primeiro_char(s: &str) -> &str {
    &s[..1]
}

Regra 3 — Se um dos parâmetros é &self ou &mut self, o lifetime de self é atribuído a todas as referências de saída:

struct Parser {
    input: String,
}

impl Parser {
    // O compilador infere que o retorno vive tanto quanto &self
    fn primeiro_token(&self) -> &str {
        &self.input[..self.input.find(' ').unwrap_or(self.input.len())]
    }
}

Se, após aplicar as três regras, ainda houver ambiguidade, o compilador exige anotações explícitas. Veja o erro E0106: Lifetime Ausente para exemplos detalhados.

Lifetimes em Structs

Quando uma struct contém referências, ela deve declarar lifetimes:

#[derive(Debug)]
struct Trecho<'a> {
    texto: &'a str,
    inicio: usize,
    fim: usize,
}

impl<'a> Trecho<'a> {
    fn novo(texto: &'a str, inicio: usize, fim: usize) -> Self {
        Trecho { texto, inicio, fim }
    }

    fn conteudo(&self) -> &str {
        &self.texto[self.inicio..self.fim]
    }
}

fn main() {
    let texto = String::from("Rust é incrível para sistemas");
    let trecho = Trecho::novo(&texto, 0, 4);
    println!("Trecho: {:?}, Conteúdo: '{}'", trecho, trecho.conteudo());
    // Trecho: Trecho { texto: "Rust é incrível para sistemas", inicio: 0, fim: 4 }, Conteúdo: 'Rust'
}

A struct Trecho<'a> garante que a referência texto viverá pelo menos tanto quanto a instância da struct. Se tentarmos criar um Trecho que sobreviva à String original, o compilador emitirá o erro E0597.

Múltiplos Lifetimes

Às vezes, diferentes referências em uma função têm lifetimes distintos:

fn escolher_contexto<'a, 'b>(
    principal: &'a str,
    fallback: &'b str,
    usar_principal: bool,
) -> &'a str
where
    'b: 'a, // 'b vive pelo menos tanto quanto 'a
{
    if usar_principal {
        principal
    } else {
        fallback // isso é seguro porque 'b: 'a
    }
}

fn main() {
    let fallback = String::from("valor padrão");
    let resultado;
    {
        let principal = String::from("valor principal");
        resultado = escolher_contexto(&principal, &fallback, true);
        println!("{}", resultado);
    }
}

O bound 'b: 'a (lê-se “‘b outlives ‘a”) garante que fallback vive pelo menos tanto quanto principal, permitindo que o retorno seja seguro.

O Lifetime 'static

O lifetime 'static indica que a referência é válida durante toda a execução do programa. Existem duas situações comuns:

1. Strings literais

let s: &'static str = "Eu vivo para sempre";
// Strings literais são embutidas no binário do programa

2. Dados no heap com Box::leak

fn criar_config() -> &'static str {
    let config = String::from("modo=produção");
    Box::leak(config.into_boxed_str())
}

fn main() {
    let config = criar_config();
    println!("Config: {}", config);
    // Nota: a memória nunca será liberada — use com cuidado!
}

Cuidado com 'static em Bounds

O bound T: 'static não significa que T é uma referência estática. Significa que T pode ser mantido indefinidamente — ou seja, não contém referências com lifetimes limitados:

use std::fmt::Display;

// T: 'static + Display aceita String, i32, etc.
// Não precisa ser &'static — pode ser um tipo owned
fn registrar<T: 'static + Display>(valor: T) {
    println!("Registrado: {}", valor);
}

fn main() {
    registrar(String::from("Hello")); // String é 'static (não contém referências)
    registrar(42i32);                 // i32 é 'static
    // registrar(&String::from("temp")); // NÃO compila — &String tem lifetime limitado
}

Lifetime Bounds em Generics

Lifetime bounds restringem como types genéricos se relacionam com lifetimes:

use std::fmt::Display;

#[derive(Debug)]
struct Rotulado<'a, T: Display + 'a> {
    rotulo: &'a str,
    valor: &'a T,
}

impl<'a, T: Display + 'a> Rotulado<'a, T> {
    fn mostrar(&self) -> String {
        format!("{}: {}", self.rotulo, self.valor)
    }
}

fn main() {
    let numero = 42;
    let rotulado = Rotulado {
        rotulo: "resposta",
        valor: &numero,
    };
    println!("{}", rotulado.mostrar());
}

O bound T: Display + 'a significa: “T implementa Display e qualquer referência dentro de T vive pelo menos tanto quanto ‘a”.

Higher-Rank Trait Bounds (HRTB)

HRTB é um recurso avançado que permite expressar que uma função funciona para qualquer lifetime. A sintaxe usa for<'a>:

fn aplicar_a_str<F>(f: F) -> String
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    let texto = String::from("   Olá, Rust!   ");
    let resultado = f(&texto);
    resultado.to_string()
}

fn main() {
    let resultado = aplicar_a_str(|s| s.trim());
    println!("'{}'", resultado); // 'Olá, Rust!'
}

O bound for<'a> Fn(&'a str) -> &'a str diz: “F é uma função que, para qualquer lifetime 'a, aceita uma &'a str e retorna uma &'a str”.

Onde HRTB Aparece na Prática

HRTB é mais comum do que parece — sempre que você usa closures com referências, o compilador insere HRTB implicitamente:

// Estas duas assinaturas são equivalentes:
fn processar1(f: impl Fn(&str) -> bool) {}
fn processar2(f: impl for<'a> Fn(&'a str) -> bool) {}

fn main() {
    processar1(|s| s.len() > 5);
    processar2(|s| s.len() > 5);
}

Outro cenário comum é com traits customizadas:

trait Processador {
    fn processar<'a>(&self, entrada: &'a str) -> &'a str;
}

struct Trimmer;

impl Processador for Trimmer {
    fn processar<'a>(&self, entrada: &'a str) -> &'a str {
        entrada.trim()
    }
}

fn executar_processador(p: &dyn Processador, texto: &str) -> String {
    p.processar(texto).to_string()
}

fn main() {
    let trimmer = Trimmer;
    let resultado = executar_processador(&trimmer, "  espaços  ");
    println!("'{}'", resultado); // 'espaços'
}

Erros Comuns com Lifetimes

1. Retornar referência a dado local

// NÃO COMPILA
fn criar_saudacao(nome: &str) -> &str {
    let saudacao = format!("Olá, {}!", nome);
    // &saudacao // ERRO: retornando referência a dado local
    // Solução: retorne String em vez de &str
    todo!()
}

// SOLUÇÃO
fn criar_saudacao_corrigido(nome: &str) -> String {
    format!("Olá, {}!", nome) // retorna String (dado owned)
}

2. Confusão com lifetime de struct

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

impl<'a> Cache<'a> {
    fn novo() -> Self {
        Cache { dados: Vec::new() }
    }

    fn adicionar(&mut self, item: &'a str) {
        self.dados.push(item);
    }

    fn buscar(&self, indice: usize) -> Option<&str> {
        self.dados.get(indice).copied()
    }
}

fn main() {
    let texto1 = String::from("primeiro");
    let texto2 = String::from("segundo");

    let mut cache = Cache::novo();
    cache.adicionar(&texto1);
    cache.adicionar(&texto2);

    println!("{:?}", cache.buscar(0)); // Some("primeiro")
}

3. Lifetime em closures retornadas

// Retornando closure que captura uma referência
fn criar_filtro<'a>(prefixo: &'a str) -> impl Fn(&str) -> bool + 'a {
    move |s: &str| s.starts_with(prefixo)
}

fn main() {
    let prefixo = String::from("rust");
    let filtro = criar_filtro(&prefixo);

    let palavras = vec!["rust-lang", "python", "rustacean", "java"];
    let filtradas: Vec<_> = palavras.into_iter().filter(|p| filtro(p)).collect();
    println!("{:?}", filtradas); // ["rust-lang", "rustacean"]
}

Para mais detalhes sobre erros de lifetime, veja:

Aplicações no Mundo Real

Parser com referências zero-copy

#[derive(Debug)]
struct CsvRow<'a> {
    campos: Vec<&'a str>,
}

fn parse_csv<'a>(linha: &'a str) -> CsvRow<'a> {
    CsvRow {
        campos: linha.split(',').map(|c| c.trim()).collect(),
    }
}

fn main() {
    let dados = String::from("nome, idade, cidade\nAna, 30, São Paulo\nCarlos, 25, Rio");

    let linhas: Vec<CsvRow> = dados.lines().map(|l| parse_csv(l)).collect();

    for (i, linha) in linhas.iter().enumerate() {
        println!("Linha {}: {:?}", i, linha.campos);
    }
}

Neste parser, CsvRow faz referência diretamente ao texto original sem copiar dados — uma técnica chamada zero-copy parsing que é extremamente eficiente.

Builder pattern com lifetimes

struct Query<'a> {
    tabela: &'a str,
    condicoes: Vec<String>,
    limite: Option<usize>,
}

impl<'a> Query<'a> {
    fn nova(tabela: &'a str) -> Self {
        Query {
            tabela,
            condicoes: Vec::new(),
            limite: None,
        }
    }

    fn filtrar(mut self, condicao: &str) -> Self {
        self.condicoes.push(condicao.to_string());
        self
    }

    fn limitar(mut self, n: usize) -> Self {
        self.limite = Some(n);
        self
    }

    fn construir(&self) -> String {
        let mut sql = format!("SELECT * FROM {}", self.tabela);
        if !self.condicoes.is_empty() {
            sql.push_str(" WHERE ");
            sql.push_str(&self.condicoes.join(" AND "));
        }
        if let Some(limite) = self.limite {
            sql.push_str(&format!(" LIMIT {}", limite));
        }
        sql
    }
}

fn main() {
    let query = Query::nova("usuarios")
        .filtrar("idade > 18")
        .filtrar("ativo = true")
        .limitar(10)
        .construir();

    println!("{}", query);
    // SELECT * FROM usuarios WHERE idade > 18 AND ativo = true LIMIT 10
}

Resumo Visual: Quando Anotar Lifetimes

Preciso anotar lifetimes?
│
├─ Função retorna referência?
│  ├─ Sim, com 1 parâmetro de referência → NÃO (elisão regra 2)
│  ├─ Sim, é método com &self → NÃO (elisão regra 3)
│  └─ Sim, com 2+ parâmetros → SIM, anote explicitamente
│
├─ Struct contém referências? → SIM, sempre anote
│
└─ Trait bound precisa de lifetime? → Depende do contexto

Veja Também