Ao definir estruturas que contêm referências em Rust, é necessário adicionar anotações de ciclo de vida (lifteime annotations). Isso garente que a estrutura não persista além das referências que ela contém.
#[allow(unused)]
struct TrechoImportante<'a> {
conteudo: &'a str,
}
fn main() {
let texto = String::from("Início do texto. Restante do conteúdo...");
let primeira_parte = texto.split('.').next().expect("Delimitador não encontrado");
let trecho = TrechoImportante { conteudo: primeira_parte };
}
A estrutura TrechoImportante possui um campo conteudo que armazena uma string slice — uma referência. A anotação <'a> após o nome da estrutura declara um parâmetro de ciclo de vida genérico. Isso indica que qualquer instância de TrechoImportante não pode viver mais do que a referência em seu campo conteudo.
No exemplo, a instância de TrechoImportante armazena uma referência a uma porção do String pertencente à variável texto. Como texto permanece válido enquanto TrechoImportante está em uso, a referência interna é sempre válida.
Regras de Omissão de Ciclos de Vida
O compliador Rust pode, em muitos casos, inferir ciclos de vida automaticamente. Considere esta função:
fn primeira_palavra(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[0..i];
}
}
&s[..]
}
Embora primeira_palavra não tenha anotações explícitas, ela compila. Isso ocorre devido às regras de omissão de ciclos de vida (lifetime elision rules). Sem essas regras, a assinatura seria:
fn primeira_palavra<'a>(s: &'a str) -> &'a str { ... }
As regras de omissão são:
- Cada parâmetro de referência recebe seu próprio ciclo de vida.
- Se há exatamente um ciclo de vida de entrada, ele é atribuído a todos os ciclos de vida de saída.
- Se há múltiplos ciclos de vida de entrada, mas um deles é
&selfou&mut self, o ciclo de vida deselfé atribuído aos ciclos de vida de saída.
Aplicando a regra 1 à primeira_palavra: há um parâmetro de referência (s), então recebe um ciclo de vida 'a. Pela regra 2, como há apenas um ciclo de vida de entrada, ele é atribuído ao tipo de retorno. Portanto, o ciclo de vida é inferido corretamente.
Agora, considere uma função com dois parâmetros:
fn mais_longa(a: &str, b: &str) -> &str { ... }
Após a regra 1:
fn mais_longa<'a, 'b>(a: &'a str, b: &'b str) -> &str { ... }
As regras 2 e 3 não se aplicam (múltiplos ciclos de entrada, sem self). O ciclo de vida de saída permanece indeterminado, exigindo uma anotação explícita.
Anotações em Métodos
Ao implementar métodos para uma estrutura com ciclo de vida, as regras de omissão muitas vezes simplificam a escrita.
impl<'a> TrechoImportante<'a> {
fn nivel(&self) -> i32 {
3
}
}
O ciclo de vida 'a é declarado após impl, pois faz parte do tipo. A regra 1 não exige anotação para &self em métodos sem retorno de referência.
impl<'a> TrechoImportante<'a> {
fn anunciar_e_retornar(&self, aviso: &str) -> &str {
println!("Aviso: {}", aviso);
self.conteudo
}
}
Aqui, a regra 3 se aplica: como &self é um parâmetro de entrada, seu ciclo de vida é atribuído ao retorno. Isso equivale a:
fn anunciar_e_retornar<'a, 'b>(&'a self, aviso: &'b str) -> &'a str { ... }
Se uma estrutura contém uma referência, sua instância não pode viver mais do que essa referência. Caso contrário, ocorreria uma dangling reference, o que Rust impede em tempo de compilação.
Ciclo de Vida Estático
O ciclo de vida 'static indica que uma referência pode persistir durante toda a execução do programa. Todas as string literals possuem ciclo de vida 'static:
let s: &'static str = "Esta referência é estática.";
Esses dados estão embutidos no binário do programa. Embora o compilador sugira 'static em alguns erros, é importante avaliar se a referência realmente precisa viver indefinidamente. Frequentemente, o real problema é um ciclo de vida incompatível ou uma referência inválida.
Combinando Generics, Trait Bounds e Lifetimes
É possível usar parâmetros de ciclo de vida juntamente com tipos genéricos e trait bounds.
use std::fmt::Display;
fn mais_longa_com_aviso<'a, T>(x: &'a str, y: &'a str, aviso: T) -> &'a str
where
T: Display,
{
println!("Mensagem: {}", aviso);
if x.len() > y.len() {
x
} else {
y
}
}
A função mais_longa_com_aviso recebe duas slices com o mesmo ciclo de vida 'a e um parâmetro genérico T que deve implementar Display. O parâmetro aviso é impresso antes da comparação. Os parâmetros de ciclo de vida e genéricos são declarados na mesma lista <'a, T>.