Por que o Rust obriga a especificar tempos de vida? E como isso simplifica a vida?
\n\nSe você está estudando Rust, mais cedo ou mais tarde encontrará essa sintaxe misteriosa: 'a, 'b... E surge uma pergunta natural: «Por que eu preciso especificar explicitamente os tempos de vida? Outras linguagens conseguem se virar!»
Essa pergunta é uma das mais populares no Stack Overflow (milhares de visualizações). E não é surpresa: o conceito de lifetimes no Rust parece complicado até que você entenda sua essência. Vamos analisar sem teoria entediante, com exemplos práticos.
\n\nResposta curta: segurança sem coletor de lixo
\n\nO Rust garante segurança de memória em tempo de compilação. Ele precisa saber exatamente quanto tempo as referências vivem para evitar referências pendentes (dangling references). Em Java ou Go, o coletor de lixo cuida disso. Em C++, você é responsável pela memória (e frequentemente erra). O Rust escolheu um terceiro caminho: o compilador verifica tudo estaticamente.
\n\nMas por que o compilador não consegue adivinhar sozinho? Ele consegue — em 80% dos casos. Isso é chamado de lifetime elision (regras de inferência implícita). No entanto, em situações complexas, ele precisa da sua ajuda.
\n\nRegra nº 1: O compilador não é vidente
\n\nVeja esta função:
\n\nfn longest(x: &str, y: &str) -> &str {\n if x.len() > y.len() { x } else { y }\n}\n\nÀ primeira vista, tudo parece lógico. Mas o Rust emitirá um erro: «missing lifetime specifier». Por quê?
\n\nImagine que você passou x — uma referência a uma string que vive 5 segundos, e y — uma referência a uma string que vive 1 segundo. A função pode retornar qualquer uma delas. Qual será o tempo de vida da referência retornada? O compilador não sabe!
A solução é dizer explicitamente: «a referência retornada viverá tanto quanto ambas as referências de entrada»:
\n\nfn longest<'a>(x: &'a str, y: &'a str) -> &'a str {\n if x.len() > y.len() { x } else { y }\n}\n\nO símbolo 'a é uma anotação de tempo de vida. Estamos dizendo: «todas as três referências devem ter o mesmo tempo de vida mínimo». Agora o compilador pode verificar se o código chamador não destruirá os dados antes do tempo.
Regra nº 2: Referências diferentes — vidas diferentes
\n\nUm erro comum de iniciantes é pensar que todos os 'a devem ser iguais. Na verdade, você pode especificar tempos de vida diferentes:
fn example<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {\n x // Ok, retornamos uma referência com tempo de vida 'a\n // y // Erro! y vive 'b, mas prometemos 'a\n}\n\nAqui, dizemos explicitamente: x e y podem ter tempos de vida diferentes. Mas só podemos retornar a referência cujo tempo de vida corresponde ao prometido no tipo de retorno.
Regra nº 3: Estruturas também podem armazenar referências
\n\nAo criar uma estrutura que contém referências, os tempos de vida no Rust devem ser especificados obrigatoriamente:
\n\nstruct Book<'a> {\n title: &'a str,\n author: &'a str,\n}\n\nfn main() {\n let title = String::from("Rust Book");\n let book;\n {\n let author = String::from("John Doe");\n book = Book { title: &title, author: &author };\n } // author é destruído aqui!\n println!("{}", book.title); // Erro! book.author está pendente\n}\n\nO compilador não permitirá compilar este código, porque book.author referencia uma string já removida. Os lifetimes explícitos no Rust permitem capturar esses erros em tempo de compilação, e não em tempo de execução.
Regra nº 4: Lifetime Elision — quando não é necessário escrever
\n\nO Rust é inteligente o suficiente para inferir tempos de vida em casos simples. Aqui estão os casos em que você não precisa especificar:
\n\n- \n
- Uma referência de entrada — o tempo de vida da saída é igual ao tempo de