Non-lexical lifetimes em Rust: o que é e como simplifica o código

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

O que são non-lexical lifetimes? Conhecendo a principal melhoria do Rust

\n\n

Todo mundo que escreveu em Rust por pelo menos algumas semanas já passou por uma situação: o código é absolutamente lógico, seguro, mas o compilador (ou melhor, seu rigoroso borrow checker) se recusa a compilá-lo. Você olha para o erro e pensa: "Mas eu não estou mais usando esta referência! Por que ele está reclamando?".

\n\n

Até a versão Rust 1.31 (edição 2018), a resposta era simples: "Porque o tempo de vida da variável (lifetime) é determinado pelo escopo — lexicalmente". Isso são os lexical lifetimes. A partir do Rust 2018, o compilador ficou mais inteligente. Ele ganhou os non-lexical lifetimes (NLL) — tempos de vida não lexicais.

\n\n

Neste artigo, vamos entender o que é essa ferramenta, por que seu surgimento foi uma revolução para os desenvolvedores Rust, e como ela torna o código mais limpo e intuitivo. Não vamos nos aprofundar nos meandros da teoria dos tipos, mas sim nos concentrar em exemplos práticos.

\n\n

O problema dos lexical lifetimes: por que o borrow checker era muito rigoroso

\n\n

Vamos lembrar como funciona o empréstimo (borrowing) em Rust. Temos as regras:

\n
    \n
  • A qualquer momento, pode haver uma referência mutável (&mut T) ou qualquer número de referências imutáveis (&T).
  • \n
  • As referências devem ser sempre válidas (não se pode referenciar dados deletados).
  • \n
\n\n

Antes do NLL, o compilador determinava o tempo de vida de uma referência pelo seu escopo léxico. Ou seja, de { até }, onde a variável é declarada. Se uma referência é criada dentro de um bloco, ela "vive" até o final desse bloco, mesmo que você não a use mais.

\n\n

Veja este exemplo clássico que não compilava nas versões antigas do Rust:

\n\n
fn main() {\n    let mut data = vec![1, 2, 3];\n\n    let x = &data[0];  // referência imutável\n\n    data.push(4);       // tentativa de empréstimo mutável\n\n    println!("{}", x);  // uso da referência imutável\n}
\n\n

Do ponto de vista humano: criamos uma referência x, depois modificamos o vetor, e depois usamos x. Mas o vetor data ao chamar push poderia realocar a memória, e a referência x se tornaria inválida. O compilador está certo — isso é perigoso.

\n\n

Mas e se usarmos a referência antes da modificação? Eis a verdadeira dor:

\n\n
fn main() {\n    let mut name = String::from("Alice");\n\n    let first_char = &name[0..1]; // referência imutável\n    println!("First char: {}", first_char); // usamos ela\n\n    name.push_str(" Johnson"); // modificamos a string\n    println!("Full name: {}", name);\n}
\n\n

Este código é seguro. Usamos a referência first_char, a exibimos, e só depois modificamos a string. Mas antes do Rust 2018, o compilador gerava um erro! Por quê? Porque o tempo de vida léxico da referência first_char durava até o final da função main, ou seja, até a chave de fechamento. E um empréstimo mutável (push_str) dentro desse intervalo é proibido.

\n\n

Os desenvolvedores tinham que usar gambiarras: introduzir blocos adicionais {} para limitar o tempo de vida da referência manualmente:

\n\n
fn main() {\n    let mut name = String::from("Alice");\n\n    {\n        let first_char = &name[0..1];\n        println!("First char: {}", first_char);\n    } // a referência first_char morre aqui\n\n    name.push_str(" Johnson");\n    println!("Full name: {}", name);\n}
\n\n

Isso funcionava, mas era inconveniente, poluía o código com chaves extras e dificultava a leitura. Especialmente em funções complexas com vários empréstimos.

\n\n

Non-lexical lifetimes (NLL): como funciona

\n\n

A ideia do NLL é genial em sua simplicidade: o tempo de vida de uma referência não é determinado pelo escopo, mas sim por onde ela é realmente usada.

Blogs

Book Recommendations