O que está escondido por trás de uma aspa solitária? Conhecendo o lifetime em Rust
Se você já abriu um código em Rust e viu construções estranhas como &'a str ou fn foo<'a>(), certamente se perguntou: "O que são esses apóstrofos?". Você não está sozinho — esta pergunta reuniu milhares de visualizações no Stack Overflow e se tornou uma verdadeira dor de cabeça para iniciantes.
Hoje vamos entender de uma vez por todas esses símbolos misteriosos. Spoiler: por trás das aspas está escondido um dos conceitos mais poderosos do Rust — o tempo de vida (lifetime). E sim, não é tão assustador quanto parece!
Neste artigo, falarei não apenas sobre a sintaxe, mas também sobre como ler e usar corretamente os identificadores com apóstrofo. Vamos lá!
Identificadores com apóstrofo: não são apenas enfeites
Em Rust, identificadores que começam com aspas simples (por exemplo, 'a, 'static, 'b) são anotações de tempo de vida. Eles não afetam quanto tempo uma variável vive, mas sim informam ao compilador sobre as relações entre os tempos de vida de diferentes referências.
Imagine que você é um detetive e o compilador Rust é seu assistente. Você diz: "Essas duas referências devem viver pelo mesmo tempo" e as marca com o rótulo 'a. O compilador verifica se você não está violando essa regra. Se violar, você recebe um erro em tempo de compilação, não em tempo de execução.
Por que elas têm essa aparência?
A sintaxe com apóstrofo não foi escolhida por acaso. Em Rust, o apóstrofo é usado para denotar rótulos (labels) — são como "nomes" para intervalos de tempo. Essa abordagem também é encontrada em outras linguagens, mas em Rust tornou-se um elemento-chave do sistema de segurança de memória.
Aqui estão alguns exemplos onde você encontrará esses rótulos:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str— função que retorna uma referência que vive não mais que ambos os argumentos.struct Book<'a> { title: &'a str }— estrutura contendo uma referência com tempo de vida.&'static str— referência a uma string estática que vive durante todo o programa.
Como ler e entender o lifetime?
Vamos analisar alguns exemplos reais de código. Eles ajudarão você a parar de temer os apóstrofos e começar a usá-los.
Exemplo 1: Função com um tempo de vida
Suponha que estamos escrevendo uma função que retorna a mais longa de duas strings:
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("hello"); let result; { let string2 = String::from("world"); result = longest(&string1, &string2); } // println!("{}", result); // Erro de compilação!}O que está acontecendo aqui? Dizemos ao compilador: "O resultado viverá não mais que x e y juntos". Quando string2 sai do escopo, result torna-se inválido. O compilador vê isso e não nos deixa dar um tiro no pé.
Exemplo 2: Estrutura com referência
Frequentemente, iniciantes tentam armazenar referências em estruturas e recebem erros. A solução é especificar explicitamente o tempo de vida:
#[derive(Debug)]struct Book<'a> { title: &'a str, author: &'a str,}
fn main() { let title = String::from("The Rust Programming Language"); let author = String::from("Steve Klabnik"); let book = Book { title: &title, author: &author, }; println!("{:?}", book);}Aqui, 'a significa que title e author devem viver pelo menos tanto quanto a própria estrutura Book. Se você tentar excluir