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
- The Rust Reference: Lifetime parameters — Documentação oficial sobre parâmetros de lifetime em itens genéricos
- Rust Book: Validating References with Lifetimes — Capítulo completo sobre lifetimes no livro oficial de Rust
- Rustonomicon: Subtyping and Variance — Explicação detalhada sobre variância e subtyping em Rust
- Rust by Example: Lifetimes — Exemplos práticos de lifetimes em structs, métodos e funções
- The Rust RFC Book: Lifetime elision — RFC original que introduziu as regras de elisão de lifetimes
- Rust Design Patterns: Lifetime management — Padrões de design para gerenciamento de lifetimes e RAII em Rust