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
- The Rust Programming Language, Cap. 10.3 — Validating References with Lifetimes — https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html
- Rust by Example — Lifetimes — https://doc.rust-lang.org/rust-by-example/scope/lifetime.html
- "Common Rust Lifetime Misconceptions" — pretzelhammer — artigo essencial que desmistifica lifetimes — https://github.com/pretzelhammer/rust-blog/blob/master/posts/common-rust-lifetime-misconceptions.md
- "Rust Lifetimes" — Jon Gjengset — aula aprofundada em vídeo — https://www.youtube.com/watch?v=rAl-9HwD858
- Rust Reference — Lifetime elision — regras completas de elisão — https://doc.rust-lang.org/reference/lifetime-elision.html
- Rustlings, seção
lifetimes— https://github.com/rust-lang/rustlings