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:
- E0106: Lifetime Ausente — quando faltam anotações de lifetime
- E0597: Valor Não Vive o Suficiente — quando um valor é destruído antes de suas referências
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
- Ownership e Borrowing: O Coração do Rust — fundamento necessário para entender lifetimes
- Traits e Generics em Rust — lifetime bounds em contexto genérico
- Smart Pointers em Rust — alternativas a referências com lifetimes
- Closures em Rust — lifetimes em closures capturadas
- E0106: Lifetime Ausente — como resolver anotações faltantes
- E0597: Valor Não Vive o Suficiente — quando dados saem do escopo cedo demais
- E0621: Lifetime Mismatch — quando lifetimes não são compatíveis