Skip to main content

Apóstrofe, Lifetime | Rust

  • Apóstrofe, Lifetime | Rust

O que é lifetime

Lifetime é o tempo em que uma referência é válida dentro de um programa Rust.

Rust usa lifetimes para impedir que uma referência aponte para um valor que já foi destruído.

Em termos simples:

let referencia = &valor;

A referência referencia só pode ser usada enquanto valor ainda existir.

Esse controle faz parte do modelo de ownership, borrowing e lifetimes, que é uma das bases da segurança de memória do Rust. O livro oficial de Rust trata lifetimes junto com ownership, borrowing, generics e traits.


Problema que lifetime resolve

Em linguagens como C/C++, é possível criar uma referência ou ponteiro para algo que já saiu da memória.

Exemplo conceitual inválido:

fn main() {
let referencia;

{
let texto = String::from("Rust");
referencia = &texto;
}

println!("{}", referencia);
}

Esse código não compila.

O motivo é que texto existe apenas dentro deste bloco:

{
let texto = String::from("Rust");
referencia = &texto;
}

Quando o bloco termina, texto é destruído. Depois disso, referencia apontaria para um valor inexistente.

Rust impede isso em tempo de compilação.


Regra central

Uma referência nunca pode viver mais do que o valor referenciado.

referência <= valor original

Ou seja, se você tem:

let texto = String::from("Rust");
let r = &texto;

r só é válido enquanto texto ainda existir.


Lifetime implícita

Na maior parte do tempo, você não escreve lifetime manualmente.

Exemplo:

fn mostrar(texto: &str) {
println!("{}", texto);
}

Mesmo sem escrever lifetime, ela existe.

O compilador entende algo como:

fn mostrar<'a>(texto: &'a str) {
println!("{}", texto);
}

Mas como a lifetime é simples, Rust consegue inferir sozinho.


Lifetime explícita

Você escreve lifetime explicitamente quando o compilador precisa entender a relação entre referências.

Exemplo:

fn maior<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

Aqui temos:

<'a>

Isso declara uma lifetime chamada 'a.

Depois usamos essa lifetime nas referências:

x: &'a str
y: &'a str
-> &'a str

Isso significa:

A função recebe duas referências válidas por uma lifetime 'a e retorna uma referência que também é válida por essa mesma lifetime.


O que 'a realmente significa

'a não é uma variável comum.

Ela é um nome simbólico para uma duração de validade.

Este código:

fn maior<'a>(x: &'a str, y: &'a str) -> &'a str

não está dizendo que x e y vivem exatamente o mesmo tempo.

Está dizendo que o retorno será válido enquanto for seguro em relação às referências recebidas.

Na prática, o retorno não pode viver mais do que a menor lifetime entre x e y.


Lifetime não aumenta vida de variável

Este é um ponto importante.

Lifetime não faz um valor viver mais.

Errado pensar:

Vou colocar 'a para a variável durar mais.

Lifetime apenas descreve regras para o compilador.

Exemplo inválido:

fn criar_referencia<'a>() -> &'a str {
let texto = String::from("Rust");
&texto
}

Esse código não compila.

Mesmo usando 'a, texto continua sendo destruído no final da função. Rust não permite retornar uma referência para algo que será destruído.

Forma correta:

fn criar_texto() -> String {
String::from("Rust")
}

Nesse caso, você retorna o valor, não uma referência para um valor local.


Quando lifetime aparece

Lifetime aparece principalmente em:

SituaçãoExemplo
Referência com tempo de vida explícito&'a str
Função genérica sobre lifetimefn exemplo<'a>()
Struct que guarda referênciastruct Pessoa<'a>
Trait impl com referênciaimpl<'a> MinhaTrait for Tipo<'a>
Lifetime estática&'static str
Lifetime inferida/anônimaFormatter<'_>

Lifetime em funções

Caso simples sem lifetime explícita

fn primeiro(texto: &str) -> &str {
texto
}

Esse caso compila porque Rust consegue inferir que o retorno está ligado ao parâmetro texto.

Conceitualmente:

fn primeiro<'a>(texto: &'a str) -> &'a str {
texto
}

Caso com duas referências

fn maior(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}

Esse código pode gerar erro, porque Rust precisa saber se o retorno está ligado a x ou a y.

Forma correta:

fn maior<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

Aqui você informa que o retorno está relacionado às lifetimes dos parâmetros.


Lifetime em structs

Quando uma struct guarda uma referência, ela precisa declarar uma lifetime.

struct Pessoa<'a> {
nome: &'a str,
}

Isso significa:

Pessoa não é dona de nome. Ela apenas guarda uma referência para um str que precisa continuar válido enquanto Pessoa existir.

Uso correto:

fn main() {
let nome = String::from("Adolfo");

let pessoa = Pessoa {
nome: &nome,
};

println!("{}", pessoa.nome);
}

Aqui nome vive o suficiente para que pessoa.nome seja usado com segurança.


Struct com referência inválida

Exemplo inválido:

struct Pessoa<'a> {
nome: &'a str,
}

fn criar_pessoa<'a>() -> Pessoa<'a> {
let nome = String::from("Adolfo");

Pessoa {
nome: &nome,
}
}

Esse código não compila.

nome é criado dentro da função e destruído quando a função termina. A struct retornada ficaria com uma referência inválida.

Forma correta usando ownership:

struct Pessoa {
nome: String,
}

fn criar_pessoa() -> Pessoa {
Pessoa {
nome: String::from("Adolfo"),
}
}

Aqui a struct é dona da String.


Lifetime anônima '_

A lifetime '_ é usada quando existe uma lifetime, mas você não quer ou não precisa dar nome para ela.

Exemplo comum:

fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "valor")
}

'_ significa:

Rust, infira essa lifetime para mim.

É útil quando a lifetime não precisa ser reutilizada em outro ponto da assinatura.


Diferença entre '_ e 'a

SintaxeSignificadoQuando usar
'_Lifetime anônima/inferidaQuando a lifetime só aparece ali
'aLifetime nomeadaQuando precisa relacionar múltiplas referências

Exemplo com '_:

std::fmt::Formatter<'_>

Exemplo com 'a:

fn maior<'a>(x: &'a str, y: &'a str) -> &'a str

No segundo caso, 'a aparece em vários lugares para criar uma relação entre entrada e saída.


Lifetime 'static

'static é uma lifetime especial.

Ela indica que uma referência pode viver durante todo o programa.

Exemplo:

let texto: &'static str = "Rust";

Strings literais geralmente têm lifetime 'static, porque ficam embutidas no binário do programa.

Outro exemplo:

fn linguagem() -> &'static str {
"Rust"
}

Isso funciona porque "Rust" não é criado dentro da função como uma String comum. É um literal armazenado no programa.


Cuidado com 'static

Não use 'static como solução automática para erro de lifetime.

Esse é um erro comum.

Quando o compilador reclama de lifetime, muitas vezes o problema real é design:

fn exemplo() -> &'static str {
let texto = String::from("Rust");
&texto
}

Esse código não compila.

Mesmo colocando 'static, texto continua sendo destruído no final da função.

Se o dado precisa sair da função, geralmente retorne o valor:

fn exemplo() -> String {
String::from("Rust")
}

Lifetime elision

Rust possui regras de inferência chamadas lifetime elision.

Elas permitem omitir lifetimes em casos comuns.

Por isso você pode escrever:

fn tamanho(texto: &str) -> usize {
texto.len()
}

em vez de:

fn tamanho<'a>(texto: &'a str) -> usize {
texto.len()
}

E também:

fn primeiro(texto: &str) -> &str {
texto
}

em vez de:

fn primeiro<'a>(texto: &'a str) -> &'a str {
texto
}

Regras práticas de lifetime elision

De forma simplificada:

CasoRust consegue inferir?
Uma referência de entrada e uma referência de saídaSim
Várias referências de entrada e uma referência de saídaNem sempre
Método com &self retornando referênciaGeralmente sim
Struct guardando referênciaPrecisa declarar lifetime
Tipo como Formatter<'_>Pode usar '_

Exemplo com método

struct Produto {
nome: String,
}

impl Produto {
fn nome(&self) -> &str {
&self.nome
}
}

Esse código não precisa de lifetime explícita.

Rust entende que o retorno &str vem de &self.

Conceitualmente:

impl Produto {
fn nome<'a>(&'a self) -> &'a str {
&self.nome
}
}

Exemplo onde precisa de lifetime

fn escolher(x: &str, y: &str) -> &str {
x
}

Mesmo retornando sempre x, a assinatura tem duas referências de entrada. Rust pode exigir que você deixe claro de onde vem o retorno.

Forma explícita:

fn escolher<'a>(x: &'a str, _y: &str) -> &'a str {
x
}

Aqui o retorno está ligado apenas a x.

y não precisa compartilhar a mesma lifetime.


Comparando designs

Usando referência

struct Produto<'a> {
nome: &'a str,
}

Use quando a struct não deve ser dona do dado.

Usando ownership

struct Produto {
nome: String,
}

Use quando a struct deve possuir o dado.

Na dúvida, para código de aplicação, String costuma ser mais simples do que guardar &str em structs.


Erro comum

Tentar retornar referência local

fn nome() -> &str {
let texto = String::from("Rust");
&texto
}

Inválido.

texto morre no final da função.

Correto:

fn nome() -> String {
String::from("Rust")
}

Ou, se for literal:

fn nome() -> &'static str {
"Rust"
}

Tabela geral

SintaxeSignificado
&TReferência para T com lifetime inferida
&'a TReferência para T válida por 'a
&'a mut TReferência mutável válida por 'a
fn f<'a>()Função genérica sobre uma lifetime
struct X<'a>Struct que contém referência com lifetime
'_Lifetime anônima/inferida
'staticLifetime válida pelo programa inteiro

Regra prática

Use lifetime explícita quando precisar expressar relação entre referências.

Exemplo:

fn maior<'a>(x: &'a str, y: &'a str) -> &'a str

Evite lifetime explícita quando o compilador já consegue inferir.

Exemplo:

fn tamanho(texto: &str) -> usize

Use ownership quando a referência estiver complicando o design.

Exemplo:

struct Pessoa {
nome: String,
}

em vez de:

struct Pessoa<'a> {
nome: &'a str,
}