Lifetimes em Rust Explicados: Anotações, Elisão e Perguntas de Entrevista 2026

Dominar os lifetimes do Rust: anotações explícitas, regras de elisão e preparação para entrevistas técnicas com exemplos práticos.

Lifetimes em Rust Explicados: Anotações, Elisão e Perguntas de Entrevista 2026

Os lifetimes em Rust constituem o mecanismo do compilador para rastrear quanto tempo as referências permanecem válidas. Diferente de linguagens com garbage collection onde o gerenciamento de memória acontece em tempo de execução, o borrow checker do Rust valida a validade das referências em tempo de compilação, eliminando categorias inteiras de bugs de memória antes mesmo do código ser executado.

Resposta Rápida para Entrevistas

Um lifetime em Rust é uma construção em tempo de compilação que descreve o escopo durante o qual uma referência permanece válida. O borrow checker usa lifetimes para garantir que referências nunca sobrevivam aos dados que apontam, prevenindo ponteiros pendentes sem overhead em tempo de execução.

O que os Lifetimes Realmente Representam na Memória

Lifetimes não se tratam de quanto tempo os valores existem. Eles descrevem por quanto tempo as referências a esses valores permanecem válidas. Cada referência em Rust possui um lifetime, mesmo quando as anotações são omitidas.

Considere esta função que o compilador rejeita:

dangling_reference.rsrust
fn create_dangling() -> &String {
    let s = String::from("hello");
    &s  // ERRO: `s` é liberada no final da função
}

A String s existe apenas dentro do escopo da função. Retornar uma referência a ela criaria um ponteiro pendente, já que a memória da String é desalocada quando a função retorna. O borrow checker detecta isso em tempo de compilação.

A solução requer retornar um valor próprio ou garantir que os dados referenciados sobrevivam à chamada da função:

valid_return.rsrust
// Opção 1: Retornar valor próprio
fn create_owned() -> String {
    String::from("hello")
}

// Opção 2: Referenciar dados que sobrevivem à função
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

Em first_word, o &str retornado empresta da entrada s, então permanece válido enquanto s estiver. O chamador controla o lifetime da entrada.

Sintaxe e Semântica das Anotações de Lifetime

As anotações de lifetime explícitas usam a sintaxe 'a, 'b, e assim por diante. Estas não são instruções ao compilador sobre quanto tempo as referências devem viver. Elas descrevem relações entre os lifetimes de múltiplas referências.

A Referência do Rust define anotações de lifetime como parâmetros genéricos que restringem quanto tempo as referências devem permanecer válidas entre si.

lifetime_annotations.rsrust
// Ambas as entradas e a saída compartilham o mesmo lifetime 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let string1 = String::from("long string");
    let result;
    {
        let string2 = String::from("short");
        result = longest(&string1, &string2);
        println!("A mais longa: {}", result);  // Válido: ambas strings existem
    }
    // println!("{}", result);  // ERRO: string2 liberada
}

A anotação 'a informa ao compilador: a referência retornada será válida pela interseção dos lifetimes de x e y. Como string2 tem um lifetime mais curto, result não pode ser usado após string2 ser liberada.

Múltiplos lifetimes distintos expressam relações mais complexas:

multiple_lifetimes.rsrust
// A saída está ligada apenas ao lifetime do primeiro parâmetro
fn first_only<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
    x
}

fn main() {
    let owned = String::from("owned");
    let result;
    {
        let temporary = String::from("temporary");
        result = first_only(&owned, &temporary);
    }
    // result ainda válido: depende apenas de owned
    println!("{}", result);
}

Regras de Elisão de Lifetimes no Rust 2024

O compilador aplica três regras de elisão para inferir lifetimes quando as anotações são omitidas. Essas regras, documentadas no Rustonomicon, reduzem código repetitivo sem sacrificar a segurança.

As Três Regras de Elisão
  1. Cada referência de entrada recebe seu próprio parâmetro de lifetime
  2. Se existir exatamente um lifetime de entrada, ele se aplica a todas as referências de saída
  3. Se &self ou &mut self existir, seu lifetime se aplica a todas as referências de saída

Essas regras lidam com a maioria dos padrões comuns:

elision_examples.rsrust
// Regra 1: Cada entrada recebe seu próprio lifetime
fn takes_two(x: &str, y: &str) {}
// O compilador lê: fn takes_two<'a, 'b>(x: &'a str, y: &'b str) {}

// Regra 2: Um único lifetime de entrada se propaga para a saída
fn first_char(s: &str) -> &str {
    &s[0..1]
}
// O compilador lê: fn first_char<'a>(s: &'a str) -> &'a str

// Regra 3: O lifetime de &self se propaga para a saída
impl Parser {
    fn peek(&self) -> &Token {
        &self.tokens[self.position]
    }
    // O compilador lê: fn peek<'a>(&'a self) -> &'a Token
}

Quando a elisão falha, o compilador requer anotações explícitas. Isso acontece mais frequentemente com funções que retornam referências derivadas de múltiplas entradas:

elision_fails.rsrust
// ERRO: Não é possível determinar o lifetime de saída
fn ambiguous(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

// CORREÇÃO: A anotação explícita resolve a ambiguidade
fn unambiguous<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Lifetimes em Structs e a Relação Outlives

Structs que contêm referências requerem anotações de lifetime para expressar que os dados emprestados devem sobreviver à struct:

struct_lifetimes.rsrust
struct Excerpt<'a> {
    text: &'a str,
}

impl<'a> Excerpt<'a> {
    // new empresta da entrada, então Excerpt não pode sobreviver à fonte
    fn new(source: &'a str, start: usize, end: usize) -> Self {
        Excerpt { text: &source[start..end] }
    }
    
    // level() retorna dados próprios, sem lifetime na assinatura
    fn level(&self) -> u32 {
        self.text.len() as u32 / 10
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = Excerpt::new(&novel, 0, 16);
    println!("Trecho: {}", excerpt.text);
}  // novel liberada, mas excerpt já está fora de escopo

O 'a em Excerpt<'a> vincula a validade da struct ao lifetime do text emprestado. Tentar usar um Excerpt após sua String fonte ser liberada gera um erro de compilação.

Para structs com múltiplas referências, cada uma pode ter lifetimes distintos:

multiple_struct_lifetimes.rsrust
struct Comparison<'a, 'b> {
    baseline: &'a str,
    candidate: &'b str,
}

impl<'a, 'b> Comparison<'a, 'b> {
    fn new(baseline: &'a str, candidate: &'b str) -> Self {
        Comparison { baseline, candidate }
    }
    
    fn baseline_only(&self) -> &'a str {
        self.baseline
    }
}

Pronto para mandar bem nas entrevistas de Rust?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

O Lifetime 'static e Quando Usá-lo

O lifetime 'static indica dados que vivem durante toda a execução do programa. Literais de string têm esse lifetime porque estão embutidos no binário:

static_lifetime.rsrust
let s: &'static str = "Eu vivo para sempre";

// Constantes são implicitamente 'static
const CONFIG_VERSION: &str = "2.0.0";

// Globais thread-safe requerem 'static
static COUNTER: AtomicU64 = AtomicU64::new(0);

Um padrão comum envolve os bounds T: 'static, o que frequentemente confunde desenvolvedores. Esse bound significa que T não contém referências não estáticas, não que T deve ser uma referência:

static_bound.rsrust
use std::thread;

fn spawn_task<T: Send + 'static>(data: T) {
    thread::spawn(move || {
        // data movida para a thread, deve viver independentemente
        println!("Processando na thread");
    });
}

fn main() {
    let owned = String::from("dados próprios");
    spawn_task(owned);  // OK: String é 'static (possui seus dados)
    
    let reference = "emprestada";
    // spawn_task(reference);  // OK: &'static str
}

A thread pode sobreviver à função que a criou, então dados movidos para threads não devem conter referências a variáveis locais da pilha.

Erros de Lifetime Comuns e Suas Soluções

Vários padrões fazem os desenvolvedores tropeçarem consistentemente. Entender estes acelera a depuração.

Retornar referências a variáveis locais:

error_local_ref.rsrust
fn bad_split(text: &str, delimiter: char) -> (&str, &str) {
    let parts: Vec<&str> = text.split(delimiter).collect();
    (parts[0], parts[1])  // OK: parts[i] empresta de text, não do Vec
}

fn truly_bad() -> &str {
    let local = String::from("local");
    &local  // ERRO: local é liberada no final da função
}

Armazenar referências em structs com lifetimes incompatíveis:

error_struct_lifetime.rsrust
struct Cache {
    data: String,
    // view: &str,  // ERRO: precisa de parâmetro de lifetime
}

// Structs auto-referenciais requerem unsafe ou crates como ouroboros
struct CacheWithView<'a> {
    data: String,
    view: Option<&'a str>,  // Não pode apontar para self.data com segurança
}

Structs auto-referenciais, onde um campo referencia outro campo, requerem Pin com código unsafe ou crates como ouroboros. O módulo Rust smart pointers cobre esses padrões avançados.

Bounds de lifetime em implementações de traits:

trait_lifetime_bounds.rsrust
trait Processor {
    fn process<'a>(&self, input: &'a str) -> &'a str;
}

struct Prefixer {
    prefix: String,
}

impl Processor for Prefixer {
    // Não é possível retornar &format!(...) - seria temporário
    fn process<'a>(&self, input: &'a str) -> &'a str {
        input  // Deve retornar input ou parte dele
    }
}

Higher-Ranked Trait Bounds (HRTBs) para Lifetimes Genéricos

Higher-ranked trait bounds usam a sintaxe for<'a> para expressar que um tipo deve satisfazer um trait para qualquer lifetime, não apenas um específico:

hrtb.rsrust
use std::fmt::Debug;

// F deve ser chamável com qualquer lifetime 'a
fn apply_to_refs<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    let owned = String::from("test");
    let result = f(&owned);
    println!("{}", result);
}

fn identity(s: &str) -> &str { s }

fn main() {
    apply_to_refs(identity);
}

HRTBs aparecem frequentemente em APIs que aceitam closures e no ecossistema Rust async/await onde futures devem funcionar com referências de lifetimes variados.

Perguntas de Entrevista sobre Lifetimes em Rust

Entrevistas técnicas investigam a compreensão de lifetimes em múltiplos níveis de profundidade. Essas perguntas aparecem regularmente em posições de Rust.

Pergunta: Por que este código falha ao compilar?

interview_q1.rsrust
fn get_str() -> &str {
    "hello"
}

Resposta: O tipo de retorno precisa de uma anotação de lifetime explícita. Embora o literal de string tenha lifetime 'static, a assinatura da função não expressa isso. A correção: fn get_str() -> &'static str.

Pergunta: Explicar por que isso compila e se é seguro:

interview_q2.rsrust
fn longest<'a>(x: &'a str, _y: &str) -> &'a str {
    x
}

Resposta: Isso compila porque o lifetime de saída está ligado apenas a x. O parâmetro _y pode ter qualquer lifetime já que o valor de retorno não depende dele. É seguro: a validade da referência retornada depende apenas do lifetime de x.

Pergunta: O que acontece quando se tenta armazenar uma referência junto com dados próprios?

interview_q3.rsrust
struct Config<'a> {
    name: String,
    description: &'a str,
}

Resposta: Esse padrão é válido mas restringe como Config pode ser usado. A struct não pode sobreviver ao que description referencia. Para dados próprios que precisam referenciar a si mesmos, considere usar String para ambos os campos, ou as técnicas cobertas em Rust ownership e borrowing.

Pergunta: Como os lifetimes interagem com trait objects?

interview_q4.rsrust
trait Formatter {
    fn format(&self, input: &str) -> String;
}

fn get_formatter<'a>() -> Box<dyn Formatter + 'a> {
    // ...
}

Resposta: Trait objects têm um bound de lifetime 'static implícito por padrão. Escrever Box<dyn Formatter + 'a> permite explicitamente que o trait object contenha referências com lifetime 'a. Sem o bound explícito, Box<dyn Formatter> equivale a Box<dyn Formatter + 'static>.

Variância de Lifetimes: Covariância e Contravariância

Variância determina como lifetimes se relacionam quando tipos estão aninhados. Referências do Rust seguem estas regras:

  • &'a T é covariante em 'a: um lifetime mais longo pode substituir um mais curto
  • &'a mut T é invariante em T: o tipo deve corresponder exatamente
  • fn(&'a T) é contravariante em 'a: um lifetime mais curto pode substituir um mais longo
variance.rsrust
fn covariant_example() {
    let s: &'static str = "static";
    let r: &str = s;  // OK: 'static vive mais que qualquer 'a
}

fn invariant_example() {
    let mut vec: Vec<&'static str> = vec!["a"];
    // let s = String::from("local");
    // vec.push(&s);  // ERRO: &s não é &'static str
}

Entender variância importa ao projetar APIs genéricas que aceitam ou retornam referências. O artigo Rust traits e generics cobre padrões genéricos avançados.

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Anotações de Lifetime na Prática: Pontos Chave

  • Lifetimes descrevem validade de referências, não existência de valores. O borrow checker os usa para prevenir ponteiros pendentes em tempo de compilação.
  • Regras de elisão lidam com a maioria dos casos automaticamente. Anotações explícitas se tornam necessárias ao retornar referências derivadas de múltiplas entradas.
  • Lifetimes de struct expressam a relação outlives: qualquer struct contendo referências deve ser parametrizada pelos lifetimes dessas referências.
  • O bound 'static em genéricos significa "não contém referências não estáticas", não "deve ser uma referência".
  • Structs auto-referenciais requerem tratamento especial através de Pin, código unsafe, ou crates auxiliares.
  • Perguntas de entrevista focam em entender por que o código falha ao compilar e como anotações de lifetime mudam as garantias de validade.
Desafio do dia

Você saberia encontrar o bug em Rust?

Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador da SharpSkill

Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.

Atualizado em 27 de agosto de 2026

Compartilhar

Artigos relacionados