Lifetimes em structs e impl

1. Por que Structs Precisam de Lifetimes?

1.1. O problema de armazenar referências em structs

Em Rust, toda referência possui um lifetime — o período durante o qual ela é válida. Quando tentamos armazenar uma referência em uma struct sem anotar o lifetime, o compilador não consegue verificar se a referência ainda será válida quando a struct for usada. Considere o exemplo:

// Isto não compila!
struct Livro {
    titulo: &str,
    autor: &str,
}

O compilador rejeita este código porque não sabe por quanto tempo as referências &str vão viver. A solução é parametrizar a struct com lifetimes.

1.2. Sintaxe básica: struct Exemplo<'a>

A anotação de lifetime informa ao compilador que as referências dentro da struct devem viver pelo menos enquanto a própria struct existir:

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

fn main() {
    let titulo = String::from("1984");
    let autor = String::from("George Orwell");

    let livro = Livro {
        titulo: &titulo,
        autor: &autor,
    };

    println!("{} por {}", livro.titulo, livro.autor);
}

Aqui, 'a representa o lifetime compartilhado entre a struct e as strings referenciadas. O compilador garante que as strings não serão destruídas antes do livro.

1.3. Lifetimes múltiplas em structs

Quando os campos têm origens diferentes, usamos lifetimes distintos:

struct Par<'a, 'b> {
    primeiro: &'a str,
    segundo: &'b str,
}

fn main() {
    let a = String::from("Olá");
    let resultado;
    {
        let b = String::from("Mundo");
        let par = Par {
            primeiro: &a,
            segundo: &b,
        };
        resultado = par.primeiro; // OK: 'a vive mais
        // resultado = par.segundo; // Erro: 'b não vive o suficiente
    }
    println!("{}", resultado);
}

2. Implementando Métodos em Structs com Lifetimes

2.1. Declaração de impl<'a>

Para implementar métodos em uma struct com lifetime, declaramos o bloco impl com o mesmo parâmetro:

struct Mensagem<'a> {
    conteudo: &'a str,
}

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

    fn conteudo(&self) -> &'a str {
        self.conteudo
    }
}

fn main() {
    let msg = Mensagem::novo("Rust é seguro");
    println!("{}", msg.conteudo());
}

2.2. Métodos com lifetimes diferentes

Um método pode introduzir lifetimes adicionais, independentes do lifetime da struct:

struct Analisador<'a> {
    dados: &'a [u8],
}

impl<'a> Analisador<'a> {
    fn comparar(&self, outros_dados: &'b [u8]) -> bool
    where
        'b: 'a
    {
        self.dados.len() == outros_dados.len()
    }
}

2.3. Métodos que retornam referências

O lifetime de retorno deve estar vinculado ao lifetime da struct ou ao parâmetro recebido:

struct Container<'a> {
    valor: &'a i32,
}

impl<'a> Container<'a> {
    fn obter(&self) -> &i32 {
        self.valor  // Lifetime inferido como 'a
    }

    fn maior_que(&self, outro: &i32) -> &i32 {
        if *self.valor > *outro {
            self.valor
        } else {
            outro // Erro! Lifetime não corresponde
        }
    }
}

3. Relação entre Lifetimes da Struct e dos Métodos

3.1. Lifetimes independentes

Os lifetimes de métodos podem ser completamente independentes dos da struct:

struct Grade<'a> {
    nome: &'a str,
}

impl<'a> Grade<'a> {
    // 'b é independente de 'a
    fn anexar_sufixo<'b>(&self, sufixo: &'b str) -> String {
        format!("{} {}", self.nome, sufixo)
    }
}

3.2. Elision rules em métodos

Rust aplica regras de elisão (omissão) de lifetimes em métodos. Quando um método recebe &self ou &mut self, o compilador assume que o lifetime de retorno é o mesmo de self:

struct Texto<'a>(&'a str);

impl<'a> Texto<'a> {
    // Equivalente a: fn primeiro_caractere<'b>(&'b self) -> &'b str
    fn primeiro_caractere(&self) -> &str {
        &self.0[0..1]
    }
}

3.3. Erros comuns: conflito com self

Um erro frequente ocorre quando tentamos retornar uma referência com lifetime diferente de self:

struct Par<'a, 'b> {
    x: &'a str,
    y: &'b str,
}

impl<'a, 'b> Par<'a, 'b> {
    fn retornar_x(&self) -> &'a str {
        self.x
    }

    // Erro: lifetime do retorno não especificado
    fn retornar_maior(&self) -> &??? str {
        if self.x.len() > self.y.len() {
            self.x
        } else {
            self.y
        }
    }
}

4. Covariância e Variância em Structs com Lifetimes

4.1. Conceitos de variância

A variância determina como subtipos se relacionam com lifetimes:

  • Covariante: &'a T — se 'a: 'b (vive mais), então &'a T é subtipo de &'b T
  • Invariante: &'a mut T — não permite substituição de lifetimes
  • Contravariante: fn(T) — inverte a relação (raro em Rust)

4.2. Impacto no design de structs

// Covariante: seguro
struct Leitura<'a> {
    dados: &'a [u8],
}

// Invariante: mais restritivo
struct Escrita<'a> {
    dados: &'a mut [u8],
}

fn processar<'a>(leitura: Leitura<'a>, escrita: Escrita<'a>) {
    // Escrita<'a> não pode receber um &'b mut [u8] com 'b diferente
}

4.3. Exemplo com Cell e invariância

Cell<T> torna o tipo invariante em relação ao lifetime, prevenindo certos padrões inseguros:

use std::cell::Cell;

struct Exemplo<'a> {
    referencia: Cell<&'a str>,
}

fn problema<'a>(ex: &Exemplo<'a>) {
    // Cell força invariância: não podemos encurtar o lifetime
    let curta = String::from("temp");
    // ex.referencia.set(&curta); // Erro de lifetime!
}

5. Estratégias para Evitar Lifetimes em Structs

5.1. Tipos owned como alternativa

Quando possível, usar tipos owned simplifica o código:

// Com lifetime
struct LivroRef<'a> {
    titulo: &'a str,
}

// Sem lifetime (recomendado quando viável)
struct LivroOwned {
    titulo: String,
}

5.2. Clonagem vs referência

struct Configuracao {
    nome: String,  // Owned: sem complexidade de lifetime
}

// Versus
struct ConfiguracaoRef<'a> {
    nome: &'a str, // Requer gerenciamento de lifetime
}

// Clonagem é mais simples, mas tem custo de performance
fn usar_clonagem(original: &str) -> Configuracao {
    Configuracao {
        nome: original.to_string(),
    }
}

5.3. Usando Cow para flexibilidade

Cow<'a, T> (Clone-on-Write) permite alternar entre owned e borrowed:

use std::borrow::Cow;

struct DadosFlexiveis<'a> {
    conteudo: Cow<'a, str>,
}

impl<'a> DadosFlexiveis<'a> {
    fn modificar(&mut self) {
        // Se for borrowed, clona; se for owned, modifica in-place
        self.conteudo.to_mut().push_str(" modificado");
    }
}

fn main() {
    let owned = DadosFlexiveis {
        conteudo: Cow::Owned(String::from("dados")),
    };

    let texto = String::from("referência");
    let borrowed = DadosFlexiveis {
        conteudo: Cow::Borrowed(&texto),
    };
}

6. Lifetimes em Structs Genéricas com Traits

6.1. Combinando traits com lifetimes

trait Processavel {
    fn processar(&self) -> String;
}

struct Processador<'a, T: Processavel + 'a> {
    item: &'a T,
}

impl<'a, T: Processavel + 'a> Processador<'a, T> {
    fn executar(&self) -> String {
        self.item.processar()
    }
}

6.2. O bound T: 'a

O bound T: 'a garante que qualquer referência dentro de T viva pelo menos tanto quanto 'a:

struct Armazenador<'a, T: 'a> {
    referencia: &'a T,
}

// T pode conter referências, desde que vivam por 'a
fn exemplo<'a>(x: &'a i32) -> Armazenador<'a, i32> {
    Armazenador { referencia: x }
}

6.3. Implementando um iterador com referência

struct IteradorFatia<'a, T: 'a> {
    fatia: &'a [T],
    indice: usize,
}

impl<'a, T> Iterator for IteradorFatia<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        if self.indice < self.fatia.len() {
            let item = &self.fatia[self.indice];
            self.indice += 1;
            Some(item)
        } else {
            None
        }
    }
}

7. Padrões Avançados: Múltiplos Lifetimes

7.1. Relação outlives: 'a: 'b

A sintaxe 'a: 'b significa que 'a vive pelo menos tanto quanto 'b:

struct Aninhado<'a, 'b: 'a> {
    externo: &'b str,
    interno: &'a str,  // 'a é mais curto ou igual a 'b
}

impl<'a, 'b: 'a> Aninhado<'a, 'b> {
    fn obter_externo(&self) -> &'b str {
        self.externo
    }

    fn obter_interno(&self) -> &'a str {
        self.interno
    }
}

7.2. Coerção entre lifetimes

Structs com múltiplos lifetimes permitem coerção quando as relações são respeitadas:

struct Buffer<'leitura, 'escrita: 'leitura> {
    dados: &'escrita mut [u8],
    posicao_leitura: &'leitura [u8],
}

impl<'leitura, 'escrita: 'leitura> Buffer<'leitura, 'escrita> {
    fn novo(dados: &'escrita mut [u8]) -> Self {
        let posicao = &dados[..0];
        Buffer {
            dados,
            posicao_leitura: posicao,
        }
    }
}

7.3. Caso de uso: buffer leitura/escrita

struct BufferDuplo<'a, 'b> {
    origem: &'a [u8],
    destino: &'b mut Vec<u8>,
}

impl<'a, 'b> BufferDuplo<'a, 'b> {
    fn copiar(&mut self) -> usize {
        let tamanho = self.origem.len();
        self.destino.extend_from_slice(self.origem);
        tamanho
    }
}

fn main() {
    let dados = vec![1, 2, 3, 4, 5];
    let mut saida = Vec::new();

    let mut buffer = BufferDuplo {
        origem: &dados,
        destino: &mut saida,
    };

    buffer.copiar();
    println!("Copiados {} bytes", saida.len());
}

Conclusão

Lifetimes em structs são fundamentais para escrever código Rust seguro e eficiente. Embora possam parecer complexos inicialmente, eles fornecem garantias em tempo de compilação que eliminam categorias inteiras de bugs. Dominar padrões como lifetimes múltiplos, relações de outlives e estratégias para evitar lifetimes desnecessários é essencial para qualquer desenvolvedor Rust que trabalhe com sistemas que exigem gerenciamento fino de memória.

Referências