Pular para o conteúdo

Lifetimes em Rust

Os lifetimes (tempos de vida) costumam ser o primeiro grande obstáculo de quem está começando em Rust. Esta página vai com calma: a ideia é só você reconhecer o que é um lifetime e entender por que o compilador pede um, não dominar todas as regras de uma vez.

Você já viu em borrowing que uma referência (&) permite usar um valor sem ser dono dele. Só que uma referência só é válida enquanto o valor para o qual ela aponta ainda existir. O Rust precisa garantir que você nunca fique com uma referência para algo que já foi liberado. Esse tipo de bug se chama referência pendente (dangling reference) e é uma causa clássica de travamentos em linguagens como C.

Na maioria das vezes, o Rust descobre isso sozinho olhando os escopos do seu código, e você nem precisa pensar no assunto (foi exatamente isso que você viu na página de Escopo). Mas às vezes, principalmente em funções, o compilador não consegue deduzir sozinho e pede que você deixe isso explícito. É para isso que serve uma anotação de lifetime.

Esta função devolve a mais longa de duas string slices:

fn longest(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}

Ela não compila. O erro é este:

error[E0106]: missing lifetime specifier

O compilador está dizendo que falta um especificador de lifetime. O problema é que a referência devolvida pode vir de a ou de b, e o Rust não consegue saber qual só lendo a função. Por isso, ele não tem como garantir que a referência que você recebe de volta ainda vai ser válida quando você for usá-la.

Para resolver, você dá um nome a um lifetime (por convenção, uma letra minúscula como 'a) e diz ao Rust que o lifetime do valor de retorno está ligado aos dois parâmetros:

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
fn main() {
let s1 = String::from("hello");
let s2 = String::from("hi");
let result = longest(&s1, &s2);
println!("A mais longa: {result}");
}

Saída:

A mais longa: hello
▶ Experimente você mesmo

(Deixamos "hello" e "hi" em inglês porque o resultado depende do tamanho das strings em bytes, e letras acentuadas ocupam mais de um byte.)

Pense em 'a como um rótulo, não como um valor: ele não define uma duração de verdade. Ele só diz ao Rust: “todas estas referências precisam continuar válidas pelo menos durante o mesmo trecho de código”. Depois, o compilador confere cada chamada da função para ter certeza de que isso é verdade.

Sem essa anotação, o compilador realmente não tem como saber se a referência devolvida por longest pode viver mais do que uma das strings de onde ela veio. É a anotação de lifetime que permite a ele provar, em tempo de compilação e sem nenhum custo em tempo de execução, que a referência que você recebe sempre vai ser válida.

O próximo lugar em que você vai encontrar lifetimes é em uma struct. Até agora, todo campo de struct era dono dos seus dados, como uma String. Um campo também pode ser uma referência, mas aí a struct só é válida enquanto os dados para os quais ela aponta existirem. O Rust obriga você a deixar isso escrito com um lifetime:

struct Highlight<'a> {
text: &'a str, // pega emprestado de alguma String que vive em outro lugar
}
fn main() {
let book = String::from("Call me Ishmael. Some years ago...");
let first = Highlight { text: &book[..15] };
println!("Destaque: {}", first.text);
println!("O livro inteiro tem {} bytes", book.len());
}

Saída:

Destaque: Call me Ishmael
O livro inteiro tem 34 bytes
▶ Experimente você mesmo

Leia Highlight<'a> como “um Highlight que pega algo emprestado durante o lifetime 'a”. A parte &book[..15] é uma slice: ela pega emprestados os primeiros 15 bytes de book sem copiá-los. Como first só pega emprestado, book continua podendo ser usada depois. (O texto do livro ficou em inglês porque o exemplo corta a string por posição de byte e mostra o tamanho em bytes; letras acentuadas ocupam 2 bytes e mudariam o resultado.)

O lifetime é o que permite ao Rust pegar este erro, em que a String é liberada enquanto a struct ainda aponta para dentro dela:

struct Highlight<'a> {
text: &'a str,
}
fn main() {
let highlight;
{
let book = String::from("Call me Ishmael. Some years ago...");
highlight = Highlight { text: &book[..15] };
} // book é liberada aqui...
println!("{}", highlight.text); // ...mas highlight ainda aponta para dentro dela
}
error[E0597]: `book` does not live long enough
--> src/main.rs:9:40
|
8 | let book = String::from("Call me Ishmael. Some years ago...");
| ---- binding `book` declared here
9 | highlight = Highlight { text: &book[..15] };
| ^^^^ borrowed value does not live long enough
10 | } // book é liberada aqui...
| - `book` dropped here while still borrowed
11 |
12 | println!("{}", highlight.text); // ...mas highlight ainda aponta para dentro dela
| -------------- borrow later used here

O compilador diz que book “não vive o suficiente”: ela é liberada no fim do bloco enquanto ainda está emprestada, e o empréstimo é usado depois, na linha 12.

Uma dica prática: se os lifetimes em uma struct estiverem atrapalhando, guarde uma String própria em vez de um &str. Isso copia o texto, mas a struct deixa de depender de qualquer outra coisa, e o lifetime desaparece. Muitos programadores Rust experientes começam assim e só passam para referências quando têm um motivo.

Você vai escrever 'a bem menos do que esta página pode dar a entender. Muito código Rust nunca precisa de um lifetime explícito, porque o compilador consegue deduzi-los na maioria das situações do dia a dia. Você esbarra neles principalmente quando uma função devolve uma referência e o compilador não consegue descobrir sozinho de onde ela veio. Nessa hora, lembre-se: não é um bug, é o compilador pedindo que você confirme algo que ele não consegue provar sozinho.

Escreva uma função shortest que recebe duas string slices e devolve a mais curta. Se as duas tiverem o mesmo tamanho, devolva a primeira. Chame a função com "elefante" e "gato".

Dica: ela tem o mesmo formato de longest, lá em cima. Você precisa de <'a> depois do nome e de 'a nos dois parâmetros e no tipo de retorno.

Ver solução
fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() <= b.len() { a } else { b }
}
fn main() {
let big = String::from("elefante");
let small = String::from("gato");
println!("A mais curta: {}", shortest(&big, &small));
println!("No empate, fica a primeira: {}", shortest("pato", "gato"));
}

Saída:

A mais curta: gato
No empate, fica a primeira: pato
▶ Experimente você mesmo

A slice devolvida pode vir de a ou de b, então o Rust precisa de 'a para saber que o resultado só é válido enquanto as duas entradas forem. Usar <= em vez de < faz o empate escolher a. (Escolhemos palavras sem acento de propósito: len() conta bytes, e uma letra acentuada ocupa 2.)

Crie uma struct Quote com dois campos &str, text e author. Crie uma instância em que text pega emprestados os primeiros 20 bytes da String "Fearless concurrency is a goal of Rust", e author é o literal "The Rust Book". Imprima como Fearless concurrency (The Rust Book).

Dica: assim como Highlight acima, a struct precisa de um lifetime: struct Quote<'a>, com &'a str nos dois campos. &speech[..20] é uma slice dos primeiros 20 bytes.

Ver solução
struct Quote<'a> {
text: &'a str,
author: &'a str,
}
fn main() {
let speech = String::from("Fearless concurrency is a goal of Rust");
let quote = Quote {
text: &speech[..20],
author: "The Rust Book",
};
println!("{} ({})", quote.text, quote.author);
}

Saída:

Fearless concurrency (The Rust Book)
▶ Experimente você mesmo

quote pega emprestado de speech, então não pode viver mais do que ela. O literal "The Rust Book" fica embutido no programa e vive durante toda a execução, então dura com folga o suficiente para 'a. Experimente criar speech dentro de um bloco { } interno e imprimir a citação depois que o bloco terminar, como no exemplo de erro acima, e veja o compilador impedir você. (A frase fica em inglês porque o exercício corta a string por posição de byte.)

  • Um lifetime diz ao compilador como as referências se relacionam entre si, para que ele possa descartar referências pendentes.
  • Você só precisa escrever um quando o compilador não consegue deduzi-lo sozinho. O caso mais comum são funções que devolvem uma referência.
  • 'a não define uma duração: ele liga os lifetimes de referências diferentes para que o compilador possa conferi-los.
  • Uma struct que guarda uma referência também precisa de um lifetime (struct Highlight<'a>) e não pode viver mais do que os dados que pega emprestados. Guardar uma String própria evita isso.
  • Tudo isso é verificado em tempo de compilação; não há nenhum custo em tempo de execução.

Teste o que aprendeu

1. Que problema os lifetimes resolvem?

2. Por que fn longest(a: &str, b: &str) -> &str não compilava sem uma anotação de lifetime?

3. Adicionar uma anotação de lifetime tem algum custo em tempo de execução?

4. Segundo esta página, quando é mais comum precisar escrever um lifetime explícito?

Pontuação: 0 / 4