Lifetimes — Quando o Compilador Precisa de Mais Informação

 

Chegamos ao conceito que mais intimida quem está aprendendo Rust. Lifetimes aparecem em mensagens de erro crípticas, em assinaturas de funções cheias de apóstrofos, e parecem arbitrários à primeira vista. Mas não são — são a formalização de algo que você já sabe intuitivamente: uma referência não pode sobreviver ao valor que ela referencia.

A boa notícia é que o compilador infere lifetimes automaticamente na grande maioria dos casos. Você só precisa anotá-los explicitamente quando o compilador não tem informação suficiente para fazê-lo sozinho. E quando isso acontece, a anotação não muda o comportamento do programa — ela apenas comunica sua intenção ao compilador.


O problema que lifetimes resolve

Vamos começar com um exemplo que não compila — e entender por quê:

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

fn main() {
    let string1 = String::from("longa string aqui");
    let resultado;

    {
        let string2 = String::from("xyz");
        resultado = maior_string(&string1, &string2);
    } // string2 destruída aqui

    println!("{resultado}"); // ERRO: string2 pode ter sido usada aqui
}

O compilador recusa esse código com um erro sobre lifetimes. O problema: a função retorna uma referência que pode ser s1 ou s2, dependendo do valor em tempo de execução. O compilador não sabe qual será retornada — e portanto não pode garantir que a referência retornada seja válida após string2 ser destruída.

Para resolver isso, precisamos informar ao compilador a relação entre os lifetimes dos parâmetros e do retorno.


Sintaxe de lifetime

Lifetimes são anotados com apóstrofo seguido de um nome, por convenção começando com letras minúsculas: 'a, 'b, 'resultado. O nome mais comum é simplesmente 'a:

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

A anotação 'a não define quanto tempo as referências vivem — ela descreve a relação entre elas. Aqui estamos dizendo: "a referência retornada viverá pelo menos tanto quanto a menor das duas referências de entrada." O compilador usa essa informação para verificar que o uso é seguro.

Agora o compilador pode rejeitar o uso inseguro do exemplo anterior — ele sabe que o retorno está vinculado ao lifetime de string2, que é destruída antes de resultado ser usado.

A versão segura:

fn main() {
    let string1 = String::from("longa string aqui");
    let string2 = String::from("xyz");

    let resultado = maior_string(&string1, &string2);
    println!("A maior string é: {resultado}");
    // Ambas string1 e string2 ainda existem aqui — seguro
}

Lifetimes não mudam duração de vida

Este ponto merece repetição: anotações de lifetime não mudam quanto tempo um valor vive. Elas apenas descrevem relações para que o compilador possa verificar a segurança.

Pense assim: lifetimes são para referências o que tipos são para valores. Um tipo como i32 não muda o que um valor é — descreve o que ele pode fazer. Um lifetime não muda quando uma referência expira — descreve a relação entre referências.


Lifetimes em structs

Quando uma struct contém referências, você precisa anotar o lifetime dessas referências:

#[derive(Debug)]
struct Trecho<'a> {
    conteudo: &'a str,
}

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

    fn exibir(&self) {
        println!("Trecho: '{}'", self.conteudo);
    }

    fn tamanho(&self) -> usize {
        self.conteudo.len()
    }
}

fn main() {
    let texto = String::from("Rust é uma linguagem incrível");

    let trecho = Trecho::novo(&texto, 0, 4);
    trecho.exibir(); // Trecho: 'Rust'
    println!("Tamanho: {}", trecho.tamanho()); // 4

    // trecho não pode sobreviver a texto
    // o compilador garante isso
}

Trecho<'a> diz: "uma instância de Trecho não pode sobreviver à referência conteudo que ela contém." O compilador verifica isso em todo uso de Trecho.


Regras de elisão de lifetime

Você deve estar pensando: "Mas escrevo funções com referências o tempo todo sem anotar lifetimes." É verdade — o compilador infere lifetimes automaticamente em situações comuns, seguindo três regras chamadas regras de elisão:

Regra 1: Cada parâmetro de referência recebe seu próprio lifetime implícito.

// Você escreve:
fn tamanho(s: &str) -> usize

// Compilador lê como:
fn tamanho<'a>(s: &'a str) -> usize

Regra 2: Se há exatamente um parâmetro de referência, seu lifetime é atribuído ao retorno.

// Você escreve:
fn primeira_palavra(s: &str) -> &str

// Compilador lê como:
fn primeira_palavra<'a>(s: &'a str) -> &'a str

Regra 3: Se há um parâmetro &self ou &mut self, seu lifetime é atribuído ao retorno.

// Você escreve:
impl Struct {
    fn metodo(&self) -> &str
}

// Compilador lê como:
impl<'a> Struct<'a> {
    fn metodo(&'a self) -> &'a str
}

Quando nenhuma dessas regras resolve a ambiguidade — como no caso de maior_string com dois parâmetros de referência — o compilador pede que você anote explicitamente.


Lifetimes em métodos

Métodos de structs com lifetimes seguem a regra 3 na maioria dos casos:

#[derive(Debug)]
struct Documento<'a> {
    titulo: &'a str,
    conteudo: &'a str,
}

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

    // Regra 3 se aplica — retorno tem lifetime de &self
    fn titulo(&self) -> &str {
        self.titulo
    }

    // Aqui precisamos ser explícitos — retornamos 'a, não lifetime de &self
    fn conteudo(&self) -> &'a str {
        self.conteudo
    }

    fn resumo(&self, tamanho: usize) -> &str {
        let fim = tamanho.min(self.conteudo.len());
        &self.conteudo[..fim]
    }
}

fn main() {
    let titulo = String::from("Introdução ao Rust");
    let conteudo = String::from(
        "Rust é uma linguagem de programação de sistemas \
         focada em segurança e desempenho."
    );

    let doc = Documento::novo(&titulo, &conteudo);

    println!("Título: {}", doc.titulo());
    println!("Resumo: {}", doc.resumo(30));
    println!("{:?}", doc);
}

Múltiplos lifetimes

Às vezes você precisa de lifetimes diferentes para parâmetros diferentes:

// s1 e s2 podem ter lifetimes diferentes
// o retorno tem o lifetime do MENOR dos dois — 'b
fn selecionar<'a, 'b>(s1: &'a str, _s2: &'b str, usar_primeiro: bool) -> &'a str
where
    'b: 'a, // 'b sobrevive pelo menos tanto quanto 'a
{
    if usar_primeiro { s1 } else { s1 } // simplificado
}

A notação 'b: 'a é um lifetime bound — significa que 'b deve viver pelo menos tanto quanto 'a. Isso é análogo a trait bounds, mas para lifetimes.

Na prática, múltiplos lifetimes distintos são raros em código de aplicação. Aparecem mais em bibliotecas de baixo nível.


O lifetime especial 'static

'static é um lifetime especial que significa "válido durante toda a execução do programa":

fn main() {
    // Literais de string têm lifetime 'static
    // Elas estão embutidas no binário do programa
    let s: &'static str = "Eu vivo para sempre";

    // Mensagens de erro em Box<dyn Error> frequentemente requerem 'static
    let mensagem: &'static str = "erro crítico do sistema";

    println!("{s}");
    println!("{mensagem}");
}

Literais de string como "hello" têm tipo &'static str porque estão compiladas diretamente no binário — elas existem enquanto o programa existir.

Você verá 'static frequentemente em mensagens de erro e em tipos que precisam ser enviados entre threads. Não use 'static como solução rápida para problemas de lifetime — geralmente indica que algo deve ser String (com ownership) em vez de &str (referência).


Um programa completo: analisador de texto

Vamos construir um analisador que extrai informações de um texto usando referências com lifetimes explícitos:

#[derive(Debug)]
struct Analise<'a> {
    texto_original: &'a str,
    palavras: Vec<&'a str>,
}

impl<'a> Analise<'a> {
    fn nova(texto: &'a str) -> Self {
        let palavras = texto
            .split_whitespace()
            .collect();

        Analise {
            texto_original: texto,
            palavras,
        }
    }

    fn total_palavras(&self) -> usize {
        self.palavras.len()
    }

    fn total_caracteres(&self) -> usize {
        self.texto_original.len()
    }

    fn palavras_com_tamanho(&self, tamanho: usize) -> Vec<&str> {
        self.palavras
            .iter()
            .filter(|&&p| p.len() == tamanho)
            .copied()
            .collect()
    }

    fn palavra_mais_longa(&self) -> Option<&str> {
        self.palavras
            .iter()
            .max_by_key(|&&p| p.len())
            .copied()
    }

    fn palavra_mais_curta(&self) -> Option<&str> {
        self.palavras
            .iter()
            .min_by_key(|&&p| p.len())
            .copied()
    }

    fn contem_palavra(&self, busca: &str) -> bool {
        self.palavras.iter().any(|&p| {
            p.to_lowercase() == busca.to_lowercase()
        })
    }

    fn frequencia(&self, palavra: &str) -> usize {
        self.palavras
            .iter()
            .filter(|&&p| p.to_lowercase() == palavra.to_lowercase())
            .count()
    }

    fn exibir_relatorio(&self) {
        println!("── Relatório de Análise ─────────────────");
        println!("Total de palavras  : {}", self.total_palavras());
        println!("Total de caracteres: {}", self.total_caracteres());

        if let Some(longa) = self.palavra_mais_longa() {
            println!("Palavra mais longa : '{longa}' ({} chars)", longa.len());
        }

        if let Some(curta) = self.palavra_mais_curta() {
            println!("Palavra mais curta : '{curta}' ({} chars)", curta.len());
        }

        let media = self.total_caracteres() as f64 / self.total_palavras() as f64;
        println!("Tamanho médio      : {media:.1} chars/palavra");
    }
}

fn primeira_linha<'a>(texto: &'a str) -> &'a str {
    match texto.find('\n') {
        Some(pos) => &texto[..pos],
        None      => texto,
    }
}

fn palavras_em_comum<'a>(
    analise1: &'a Analise,
    analise2: &'a Analise,
) -> Vec<&'a str> {
    analise1.palavras
        .iter()
        .filter(|&&p| analise2.contem_palavra(p))
        .copied()
        .collect()
}

fn main() {
    let texto1 = "Rust é uma linguagem de programação de sistemas \
                  Rust oferece segurança sem coletor de lixo \
                  programação em Rust é diferente mas vale a pena";

    let texto2 = "Programação de sistemas requer cuidado \
                  segurança de memória é essencial em sistemas críticos \
                  Rust resolve esse problema de forma elegante";

    let analise1 = Analise::nova(texto1);
    let analise2 = Analise::nova(texto2);

    println!("=== Texto 1 ===");
    analise1.exibir_relatorio();

    println!("\nPalavras com 4 letras: {:?}",
        analise1.palavras_com_tamanho(4));

    println!("'Rust' aparece {} vezes",
        analise1.frequencia("Rust"));

    println!("\n=== Texto 2 ===");
    analise2.exibir_relatorio();

    println!("\n=== Comparação ===");
    let comuns = palavras_em_comum(&analise1, &analise2);
    let mut comuns_unicos: Vec<&str> = comuns.clone();
    comuns_unicos.dedup();
    println!("Palavras em comum: {:?}", comuns_unicos);
}

Saída:

=== Texto 1 ===
── Relatório de Análise ─────────────────
Total de palavras  : 22
Total de caracteres: 130
Palavra mais longa : 'programação' (11 chars)
Palavra mais curta : 'é' (2 chars)
Tamanho médio      : 5.9 chars/palavra

Palavras com 4 letras: ["vale", "pena"]
'Rust' aparece 3 vezes

=== Texto 2 ===
── Relatório de Análise ─────────────────
Total de palavras  : 16
Total de caracteres: 101
Palavra mais longa : 'Programação' (11 chars)
Palavra mais curta : 'de' (2 chars)
Tamanho médio      : 6.3 chars/palavra

=== Comparação ===
Palavras em comum: ["de", "sistemas", "segurança", "de", "de", "Rust"]

A intuição por trás de lifetimes

Depois de tudo isso, vale consolidar a intuição:

Lifetimes são a maneira de Rust garantir que referências nunca apontem para memória inválida. O compilador precisa provar, antes de executar qualquer linha, que toda referência aponta para algo que ainda existe.

Na maioria do código, ele consegue fazer isso sozinho — elisão de lifetime cuida disso. Mas quando você tem múltiplas referências de entrada e uma de saída, o compilador não consegue adivinhar qual entrada "alimenta" a saída. Você precisa dizer.

Com o tempo, anotar lifetimes vai parecer natural — como declarar tipos. Você não vai lutar contra o compilador; vai usá-lo como parceiro para expressar claramente as garantias do seu código.


Generics, Traits e Lifetimes juntos

Os três conceitos que estudamos nos últimos artigos — generics, traits e lifetimes — frequentemente aparecem juntos em assinaturas de funções mais avançadas:

use std::fmt::Display;

fn exibir_maior<'a, T>(lista: &'a [T], contexto: &str) -> &'a T
where
    T: PartialOrd + Display,
{
    let mut maior = &lista[0];
    for item in lista {
        if item > maior {
            maior = item;
        }
    }
    println!("[{contexto}] Maior: {maior}");
    maior
}

fn main() {
    let numeros = vec![10, 45, 23, 78, 12];
    let maior = exibir_maior(&numeros, "inteiros");
    println!("Referência ao maior: {maior}");

    let palavras = vec!["banana", "abacaxi", "caju", "manga"];
    exibir_maior(&palavras, "frutas");
}

Saída:

[inteiros] Maior: 78
Referência ao maior: 78
[frutas] Maior: manga

Essa função tem um parâmetro de lifetime 'a, um parâmetro de tipo T com dois trait bounds, e usa a cláusula where para legibilidade. É o padrão mais completo que você encontrará em código Rust avançado.


Fontes e leituras recomendadas