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
'ae 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ção | Exemplo |
|---|---|
| Referência com tempo de vida explícito | &'a str |
| Função genérica sobre lifetime | fn exemplo<'a>() |
| Struct que guarda referência | struct Pessoa<'a> |
| Trait impl com referência | impl<'a> MinhaTrait for Tipo<'a> |
| Lifetime estática | &'static str |
| Lifetime inferida/anônima | Formatter<'_> |
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:
Pessoanão é dona denome. Ela apenas guarda uma referência para umstrque precisa continuar válido enquantoPessoaexistir.
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
| Sintaxe | Significado | Quando usar |
|---|---|---|
'_ | Lifetime anônima/inferida | Quando a lifetime só aparece ali |
'a | Lifetime nomeada | Quando 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:
| Caso | Rust consegue inferir? |
|---|---|
| Uma referência de entrada e uma referência de saída | Sim |
| Várias referências de entrada e uma referência de saída | Nem sempre |
Método com &self retornando referência | Geralmente sim |
| Struct guardando referência | Precisa 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
| Sintaxe | Significado |
|---|---|
&T | Referência para T com lifetime inferida |
&'a T | Referência para T válida por 'a |
&'a mut T | Referê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 |
'static | Lifetime 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,
}