Elisão de Lifetime e Padrões Avançados de Lifetime
Domine as regras de elisão de lifetime, padrões de lifetime de entrada/saída, bounds de lifetime e variância
Elisão de Lifetime e Padrões Avançados de Lifetime
As regras de elisão de lifetime do Rust tornam a maioria das anotações de lifetime desnecessárias. Entender exatamente quando e como a elisão funciona é fundamental para escrever Rust ergonômico.
As Três Regras de Elisão
O compilador aplica estas regras automaticamente:
Regra 1 — Lifetimes de Entrada
Cada referência elidida em parâmetros de função ganha um lifetime distinto:
// fn foo(x: &i32, y: &str)
// torna-se:
fn foo<'a, 'b>(x: &'a i32, y: &'b str) {}Regra 2 — Lifetime de Entrada Único
Se houver exatamente um lifetime de entrada, ele é atribuído a todas as referências de saída:
// fn first_word(s: &str) -> &str
// torna-se:
fn first_word<'a>(s: &'a str) -> &'a str { unimplemented!() }Regra 3 — Receptor de Método
Se houver &self ou &mut self, seu lifetime é atribuído a todas as referências de saída:
impl Widget {
// fn get_name(&self) -> &str
// torna-se:
fn get_name<'a>(&'a self) -> &'a str { unimplemented!() }
}As regras 2 e 3 só se aplicam quando não há lifetimes de saída explícitos. Se você escrever um lifetime na saída, as regras não se aplicam.
Lifetimes de Entrada vs Saída
Lifetimes de Entrada
Lifetimes em parâmetros de função:
// 'a e 'b são lifetimes de entrada
fn process<'a, 'b>(x: &'a str, y: &'b str) {}Lifetimes de Saída
Lifetimes em valores de retorno:
// 'a é um lifetime de saída (no tipo de retorno)
fn select<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}Lifetimes Apenas em Posição de Retorno
// Lifetime apenas no retorno — raro e geralmente errado
// fn foo() -> &'static str { "static" } // OK — 'static funciona
// fn bar() -> &i32 { /* para onde aponta? */ } // ErrorElisão em Assinaturas de Método
struct Container<'a> {
data: &'a str,
}
impl<'a> Container<'a> {
// Elidido: &self → saída
fn get_data(&self) -> &str {
// Expandido: fn get_data<'b>(&'b self) -> &'b str
self.data
}
// Duas refs de entrada → sem elisão automática de saída
// fn longer(&self, other: &str) -> &str {
// Expandido: fn longer<'b, 'c>(&'b self, other: &'c str) -> &'b str
fn longer<'b>(&'b self, other: &'b str) -> &'b str {
if self.data.len() > other.len() {
self.data
} else {
other
}
}
}Elisão de Lifetime e Genéricos
use std::fmt::Display;
// Elisão funciona com genéricos
fn announce_and_return<'a>(announcement: &str, x: &'a str) -> &'a str {
println!("{announcement}");
x
}
// Com trait bounds
fn longest_with_display<'a, T: Display + ?Sized>(
x: &'a T,
y: &'a T,
) -> &'a T
where
T: PartialOrd,
{
if x > y { x } else { y }
}Bounds de Lifetime em Tipos Genéricos
// T deve sobreviver a 'a
struct Wrapper<'a, T: 'a> {
value: &'a T,
}
// T deve ser 'static (sem referências não-static)
fn process_static<T: 'static>(value: T) {
std::mem::drop(value);
}
// Higher-ranked trait bounds: para qualquer lifetime
fn with_hrtb<F>(f: F)
where
F: for<'a> Fn(&'a str) -> &'a str,
{
println!("{}", f("hello"));
}Subtipagem de Lifetime (Variância)
Lifetimes podem estar em relações de subtipagem:
// Covariante: &'a T é subtipo de &'b T se 'a: 'b
// Invariante: &'a mut T — deve ser exato
// Contravariante: fn(T) — inverso
struct Covariant<'a>(&'a str); // Covariante em 'a
struct Invariant<'a>(Cell<&'a str>); // Invariante em 'a
fn main() {
let long = String::from("long lived");
let short = String::from("short");
let cov: Covariant = Covariant(&long);
// Pode atribuir a lifetime mais curto:
let _: Covariant<'_> = cov; // OK: covariante
// Invariant falharia:
// let inv: Invariant = Invariant(Cell::new(&long));
// let _: Invariant<'_> = inv; // ERROR: invariante
}Captura de Lifetime
// O impl Trait captura lifetimes da função
fn make_debug<'a>(x: &'a str) -> impl std::fmt::Debug + 'a {
x
}
// Múltiplos lifetimes em impl Trait
fn make_cloneable<'a, 'b>(x: &'a str, y: &'b str) -> impl Clone + 'a + 'b {
(x, y)
}Padrões Comuns de Elisão
// Padrão 1: Leitor
fn read(&self) -> &str { /* elidido para lifetime de &self */ }
// Padrão 2: Predicado
fn contains(&self, other: &str) -> bool { /* sem refs de saída */ }
// Padrão 3: Fábrica
fn new(value: &str) -> Self { /* elidido pela regra 2 */ }
// Padrão 4: Múltiplos retornos
fn parts(&self) -> (&str, &str) { /* todos ganham lifetime de &self */ }O Lifetime Anônimo
Use '_ para indicar explicitamente lifetime elidido:
struct Foo<'a> {
x: &'a str,
}
impl Foo<'_> { // '_ significa o lifetime elidido do impl
fn get(&self) -> &str { self.x }
}
// Em assinaturas de função
fn foo(x: &'_ str) -> &'_ str { x }Exemplo Real: Lifetimes em um Cache
use std::collections::HashMap;
struct Index<'a> {
data: &'a str,
positions: HashMap<&'a str, usize>,
}
impl<'a> Index<'a> {
fn new(data: &'a str) -> Self {
let mut positions = HashMap::new();
for (i, line) in data.lines().enumerate() {
for word in line.split_whitespace() {
positions.entry(word).or_insert(i);
}
}
Index { data, positions }
}
fn find(&self, word: &str) -> Option<&'a str> {
// Nenhum lifetime explícito necessário — elisão funciona
self.positions.get(word).map(|&line| {
self.data.lines().nth(line).unwrap_or("")
})
}
}
fn main() {
let text = String::from("apple banana\ncherry date");
let index = Index::new(&text);
if let Some(line) = index.find("cherry") {
println!("Found: {line}"); // "cherry date"
}
}Perguntas de Prática
- Quais são as três regras de elisão de lifetime?
- Quando a Regra 2 NÃO se aplica?
- Qual é a diferença entre lifetimes de entrada e saída?
- Como os lifetimes são elididos em assinaturas de método?
- O que
T: 'staticsignifica como um bound? - O que é um higher-ranked trait bound (HRTB)?
- Qual é a diferença entre covariância e invariância para lifetimes?
- O que o lifetime
'_significa? - Por que lifetimes não podem ser elididos em
fn longest(x: &str, y: &str) -> &str? - Como expressar "esta função funciona para qualquer lifetime" com um bound?