Validando Referências com Lifetimes

Beginner

This tutorial is from open-source community. Access the source code

Introdução

Bem-vindo a Validando Referências com Lifetimes. Este laboratório faz parte do Livro do Rust. Você pode praticar suas habilidades em Rust no LabEx.

Neste laboratório, discutiremos lifetimes (tempos de vida) e como eles garantem que as referências sejam válidas pelo tempo necessário, e embora os lifetimes possam parecer estranhos, abordaremos as formas comuns com que você pode encontrar a sintaxe de lifetime para ajudá-lo a se sentir confortável com o conceito.

Validando Referências com Lifetimes

Lifetimes (Tempos de vida) são outro tipo de genérico que já estamos usando. Em vez de garantir que um tipo tenha o comportamento que queremos, os lifetimes garantem que as referências sejam válidas pelo tempo que precisarmos.

Um detalhe que não discutimos em "Referências e Empréstimos" é que toda referência em Rust tem um lifetime, que é o escopo para o qual essa referência é válida. Na maioria das vezes, os lifetimes são implícitos e inferidos, assim como na maioria das vezes, os tipos são inferidos. Devemos anotar os tipos somente quando múltiplos tipos são possíveis. De forma semelhante, devemos anotar os lifetimes quando os lifetimes das referências podem estar relacionados de algumas maneiras diferentes. Rust exige que anotemos os relacionamentos usando parâmetros genéricos de lifetime para garantir que as referências reais usadas em tempo de execução sejam definitivamente válidas.

Anotar lifetimes nem mesmo é um conceito que a maioria das outras linguagens de programação possui, então isso vai parecer estranho. Embora não abordemos os lifetimes em sua totalidade neste capítulo, discutiremos as formas comuns com que você pode encontrar a sintaxe de lifetime para que possa se sentir confortável com o conceito.

Prevenindo Referências Pendentes com Lifetimes

O objetivo principal dos lifetimes é prevenir referências pendentes (dangling references), que fazem com que um programa referencie dados diferentes dos dados que ele pretende referenciar. Considere o programa na Listagem 10-16, que possui um escopo externo e um escopo interno.

fn main() {
  1 let r;

    {
      2 let x = 5;
      3 r = &x;
  4 }

  5 println!("r: {r}");
}

Listagem 10-16: Uma tentativa de usar uma referência cujo valor saiu do escopo

Nota: Os exemplos nas Listagens 10-16, 10-17 e 10-23 declaram variáveis sem dar a elas um valor inicial, então o nome da variável existe no escopo externo. À primeira vista, isso pode parecer em conflito com o fato de o Rust não ter valores nulos. No entanto, se tentarmos usar uma variável antes de dar a ela um valor, obteremos um erro em tempo de compilação, o que mostra que o Rust de fato não permite valores nulos.

O escopo externo declara uma variável chamada r sem valor inicial [1], e o escopo interno declara uma variável chamada x com o valor inicial de 5 [2]. Dentro do escopo interno, tentamos definir o valor de r como uma referência a x [3]. Então o escopo interno termina [4], e tentamos imprimir o valor em r [5]. Este código não compilará porque o valor ao qual r está se referindo saiu do escopo antes de tentarmos usá-lo. Aqui está a mensagem de erro:

error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     println!("r: {r}");
  |                   - borrow later used here

A mensagem de erro diz que a variável x "não vive tempo suficiente". A razão é que x estará fora do escopo quando o escopo interno terminar na linha 7. Mas r ainda é válido para o escopo externo; como seu escopo é maior, dizemos que ele "vive mais tempo". Se o Rust permitisse que este código funcionasse, r estaria referenciando a memória que foi desalocada quando x saiu do escopo, e qualquer coisa que tentássemos fazer com r não funcionaria corretamente. Então, como o Rust determina que este código é inválido? Ele usa um borrow checker (verificador de empréstimo).

O Borrow Checker

O compilador Rust possui um borrow checker (verificador de empréstimo) que compara escopos para determinar se todos os empréstimos são válidos. A Listagem 10-17 mostra o mesmo código da Listagem 10-16, mas com anotações mostrando os lifetimes das variáveis.

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}                         // ---------+

Listagem 10-17: Anotações dos lifetimes de r e x, nomeados 'a e 'b, respectivamente

Aqui, anotamos o lifetime de r com 'a e o lifetime de x com 'b. Como você pode ver, o bloco interno 'b é muito menor que o bloco externo de lifetime 'a. Em tempo de compilação, o Rust compara o tamanho dos dois lifetimes e vê que r tem um lifetime de 'a, mas que se refere à memória com um lifetime de 'b. O programa é rejeitado porque 'b é menor que 'a: o sujeito da referência não vive tanto quanto a referência.

A Listagem 10-18 corrige o código para que ele não tenha uma referência pendente e compile sem erros.

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {r}");   //   |       |
                          // --+       |
}                         // ----------+

Listagem 10-18: Uma referência válida porque os dados têm um lifetime mais longo que a referência

Aqui, x tem o lifetime 'b, que neste caso é maior que 'a. Isso significa que r pode referenciar x porque o Rust sabe que a referência em r sempre será válida enquanto x for válido.

Agora que você sabe onde estão os lifetimes das referências e como o Rust analisa os lifetimes para garantir que as referências sempre sejam válidas, vamos explorar os lifetimes genéricos de parâmetros e valores de retorno no contexto de funções.

Lifetimes Genéricos em Funções

Vamos escrever uma função que retorna a maior de duas string slices. Esta função receberá duas string slices e retornará uma única string slice. Depois de implementarmos a função longest, o código na Listagem 10-19 deve imprimir The longest string is abcd.

Nome do arquivo: src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

Listagem 10-19: Uma função main que chama a função longest para encontrar a maior de duas string slices

Observe que queremos que a função receba string slices, que são referências, em vez de strings, porque não queremos que a função longest assuma a propriedade de seus parâmetros. Consulte "String Slices como Parâmetros" para mais discussão sobre por que os parâmetros que usamos na Listagem 10-19 são os que queremos.

Se tentarmos implementar a função longest como mostrado na Listagem 10-20, ela não compilará.

Nome do arquivo: src/main.rs

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

Listagem 10-20: Uma implementação da função longest que retorna a maior de duas string slices, mas ainda não compila

Em vez disso, obtemos o seguinte erro que fala sobre lifetimes:

error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value,
but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

O texto de ajuda revela que o tipo de retorno precisa de um parâmetro de lifetime genérico, porque o Rust não pode dizer se a referência que está sendo retornada se refere a x ou y. Na verdade, nós também não sabemos, porque o bloco if no corpo desta função retorna uma referência a x e o bloco else retorna uma referência a y!

Quando estamos definindo esta função, não sabemos os valores concretos que serão passados para esta função, então não sabemos se o caso if ou o caso else serão executados. Também não sabemos os lifetimes concretos das referências que serão passadas, então não podemos olhar para os escopos como fizemos nas Listagens 10-17 e 10-18 para determinar se a referência que retornamos sempre será válida. O borrow checker também não pode determinar isso, porque não sabe como os lifetimes de x e y se relacionam com o lifetime do valor de retorno. Para corrigir este erro, adicionaremos parâmetros de lifetime genéricos que definem a relação entre as referências para que o borrow checker possa realizar sua análise.

Sintaxe de Anotação de Lifetime

Anotações de lifetime não alteram quanto tempo qualquer uma das referências vive. Em vez disso, elas descrevem as relações dos lifetimes de múltiplas referências entre si, sem afetar os lifetimes. Assim como as funções podem aceitar qualquer tipo quando a assinatura especifica um parâmetro de tipo genérico, as funções podem aceitar referências com qualquer lifetime especificando um parâmetro de lifetime genérico.

Anotações de lifetime têm uma sintaxe ligeiramente incomum: os nomes dos parâmetros de lifetime devem começar com um apóstrofo (') e geralmente são todos minúsculos e muito curtos, como tipos genéricos. A maioria das pessoas usa o nome 'a para a primeira anotação de lifetime. Colocamos anotações de parâmetro de lifetime após o & de uma referência, usando um espaço para separar a anotação do tipo da referência.

Aqui estão alguns exemplos: uma referência a um i32 sem um parâmetro de lifetime, uma referência a um i32 que tem um parâmetro de lifetime chamado 'a, e uma referência mutável a um i32 que também tem o lifetime 'a.

&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

Uma anotação de lifetime por si só não tem muito significado, porque as anotações são destinadas a dizer ao Rust como os parâmetros de lifetime genéricos de múltiplas referências se relacionam entre si. Vamos examinar como as anotações de lifetime se relacionam entre si no contexto da função longest.

Anotações de Lifetime em Assinaturas de Funções

Para usar anotações de lifetime em assinaturas de funções, precisamos declarar os parâmetros genéricos de lifetime dentro de colchetes angulares entre o nome da função e a lista de parâmetros, assim como fizemos com os parâmetros genéricos de tipo.

Queremos que a assinatura expresse a seguinte restrição: a referência retornada será válida enquanto ambos os parâmetros forem válidos. Esta é a relação entre os lifetimes dos parâmetros e o valor de retorno. Vamos nomear o lifetime 'a e, em seguida, adicioná-lo a cada referência, como mostrado na Listagem 10-21.

Nome do arquivo: src/main.rs

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

Listagem 10-21: A definição da função longest especificando que todas as referências na assinatura devem ter o mesmo lifetime 'a

Este código deve compilar e produzir o resultado desejado quando o usarmos com a função main na Listagem 10-19.

A assinatura da função agora diz ao Rust que, para algum lifetime 'a, a função recebe dois parâmetros, ambos string slices que vivem pelo menos tanto tempo quanto o lifetime 'a. A assinatura da função também diz ao Rust que o string slice retornado da função viverá pelo menos tanto tempo quanto o lifetime 'a. Na prática, isso significa que o lifetime da referência retornada pela função longest é o mesmo que o menor dos lifetimes dos valores referenciados pelos argumentos da função. Essas relações são o que queremos que o Rust use ao analisar este código.

Lembre-se, quando especificamos os parâmetros de lifetime nesta assinatura de função, não estamos alterando os lifetimes de nenhum valor passado ou retornado. Em vez disso, estamos especificando que o borrow checker deve rejeitar quaisquer valores que não aderirem a essas restrições. Observe que a função longest não precisa saber exatamente quanto tempo x e y viverão, apenas que algum escopo pode ser substituído por 'a que satisfaça esta assinatura.

Ao anotar lifetimes em funções, as anotações vão na assinatura da função, não no corpo da função. As anotações de lifetime se tornam parte do contrato da função, assim como os tipos na assinatura. Ter assinaturas de função contendo o contrato de lifetime significa que a análise que o compilador Rust faz pode ser mais simples. Se houver um problema com a forma como uma função é anotada ou com a forma como ela é chamada, os erros do compilador podem apontar para a parte do nosso código e as restrições com mais precisão. Se, em vez disso, o compilador Rust fizesse mais inferências sobre o que pretendíamos que fossem as relações dos lifetimes, o compilador só poderia apontar para um uso do nosso código muitos passos distante da causa do problema.

Quando passamos referências concretas para longest, o lifetime concreto que é substituído por 'a é a parte do escopo de x que se sobrepõe ao escopo de y. Em outras palavras, o lifetime genérico 'a obterá o lifetime concreto que é igual ao menor dos lifetimes de x e y. Como anotamos a referência retornada com o mesmo parâmetro de lifetime 'a, a referência retornada também será válida para a duração do menor dos lifetimes de x e y.

Vamos ver como as anotações de lifetime restringem a função longest passando referências que têm diferentes lifetimes concretos. A Listagem 10-22 é um exemplo direto.

Nome do arquivo: src/main.rs

fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {result}");
    }
}

Listagem 10-22: Usando a função longest com referências a valores String que têm diferentes lifetimes concretos

Neste exemplo, string1 é válido até o final do escopo externo, string2 é válido até o final do escopo interno e result referencia algo que é válido até o final do escopo interno. Execute este código e você verá que o borrow checker aprova; ele compilará e imprimirá The longest string is long string is long.

Em seguida, vamos tentar um exemplo que mostra que o lifetime da referência em result deve ser o menor lifetime dos dois argumentos. Vamos mover a declaração da variável result para fora do escopo interno, mas deixar a atribuição do valor à variável result dentro do escopo com string2. Em seguida, moveremos o println! que usa result para fora do escopo interno, depois que o escopo interno terminar. O código na Listagem 10-23 não compilará.

Nome do arquivo: src/main.rs

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {result}");
}

Listagem 10-23: Tentando usar result depois que string2 saiu do escopo

Quando tentamos compilar este código, obtemos este erro:

error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^^^^^^^^^^ borrowed value
does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {result}");
  |                                      ------ borrow later used here

O erro mostra que, para que result seja válido para a instrução println!, string2 precisaria ser válido até o final do escopo externo. Rust sabe disso porque anotamos os lifetimes dos parâmetros da função e os valores de retorno usando o mesmo parâmetro de lifetime 'a.

Como humanos, podemos olhar para este código e ver que string1 é maior que string2 e, portanto, result conterá uma referência a string1. Como string1 ainda não saiu do escopo, uma referência a string1 ainda será válida para a instrução println!. No entanto, o compilador não pode ver que a referência é válida neste caso. Dissemos ao Rust que o lifetime da referência retornada pela função longest é o mesmo que o menor dos lifetimes das referências passadas. Portanto, o borrow checker proíbe o código na Listagem 10-23 por possivelmente ter uma referência inválida.

Tente projetar mais experimentos que variem os valores e lifetimes das referências passadas para a função longest e como a referência retornada é usada. Faça hipóteses sobre se seus experimentos passarão ou não no borrow checker antes de compilar; então verifique se você está certo!

Pensando em Termos de Lifetimes

A maneira como você precisa especificar os parâmetros de lifetime depende do que sua função está fazendo. Por exemplo, se mudássemos a implementação da função longest para sempre retornar o primeiro parâmetro em vez do string slice mais longo, não precisaríamos especificar um lifetime no parâmetro y. O código a seguir compilará:

Nome do arquivo: src/main.rs

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

Especificamos um parâmetro de lifetime 'a para o parâmetro x e o tipo de retorno, mas não para o parâmetro y, porque o lifetime de y não tem nenhuma relação com o lifetime de x ou o valor de retorno.

Ao retornar uma referência de uma função, o parâmetro de lifetime para o tipo de retorno precisa corresponder ao parâmetro de lifetime para um dos parâmetros. Se a referência retornada não se referir a um dos parâmetros, ela deve se referir a um valor criado dentro desta função. No entanto, esta seria uma referência pendente porque o valor sairá do escopo no final da função. Considere esta tentativa de implementação da função longest que não compilará:

Nome do arquivo: src/main.rs

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

Aqui, embora tenhamos especificado um parâmetro de lifetime 'a para o tipo de retorno, esta implementação não compilará porque o lifetime do valor de retorno não está relacionado ao lifetime dos parâmetros. Aqui está a mensagem de erro que recebemos:

error[E0515]: cannot return reference to local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ^^^^^^^^^^^^^^^ returns a reference to data owned by the
current function

O problema é que result sai do escopo e é limpo no final da função longest. Também estamos tentando retornar uma referência a result da função. Não há como especificar parâmetros de lifetime que mudariam a referência pendente, e o Rust não nos permitirá criar uma referência pendente. Neste caso, a melhor solução seria retornar um tipo de dado owned em vez de uma referência, para que a função de chamada seja então responsável por limpar o valor.

Em última análise, a sintaxe de lifetime é sobre conectar os lifetimes de vários parâmetros e valores de retorno de funções. Uma vez que eles estão conectados, o Rust tem informações suficientes para permitir operações seguras de memória e proibir operações que criariam ponteiros pendentes ou violariam a segurança da memória.

Anotações de Lifetime em Definições de Structs

Até agora, as structs que definimos contêm todos os tipos owned. Podemos definir structs para conter referências, mas, nesse caso, precisaríamos adicionar uma anotação de lifetime em cada referência na definição da struct. A Listagem 10-24 tem uma struct chamada ImportantExcerpt que contém um string slice.

Nome do arquivo: src/main.rs

1 struct ImportantExcerpt<'a> {
  2 part: &'a str,
}

fn main() {
  3 let novel = String::from(
        "Call me Ishmael. Some years ago..."
    );
  4 let first_sentence = novel
        .split('.')
        .next()
        .expect("Could not find a '.'");
  5 let i = ImportantExcerpt {
        part: first_sentence,
    };
}

Listagem 10-24: Uma struct que contém uma referência, exigindo uma anotação de lifetime

Esta struct tem o campo único part que contém um string slice, que é uma referência [2]. Assim como com os tipos de dados genéricos, declaramos o nome do parâmetro genérico de lifetime dentro de colchetes angulares após o nome da struct para que possamos usar o parâmetro de lifetime no corpo da definição da struct [1]. Esta anotação significa que uma instância de ImportantExcerpt não pode viver mais tempo do que a referência que ela contém em seu campo part.

A função main aqui cria uma instância da struct ImportantExcerpt [5] que contém uma referência à primeira frase da String [4] owned pela variável novel [3]. Os dados em novel existem antes que a instância ImportantExcerpt seja criada. Além disso, novel não sai do escopo até depois que ImportantExcerpt sai do escopo, então a referência na instância ImportantExcerpt é válida.

Elisão de Lifetime

Você aprendeu que toda referência tem um lifetime e que você precisa especificar parâmetros de lifetime para funções ou structs que usam referências. No entanto, tínhamos uma função na Listagem 4-9, mostrada novamente na Listagem 10-25, que compilava sem anotações de lifetime.

Nome do arquivo: src/lib.rs

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

Listagem 10-25: Uma função que definimos na Listagem 4-9 que compilou sem anotações de lifetime, embora o parâmetro e o tipo de retorno sejam referências

A razão pela qual esta função compila sem anotações de lifetime é histórica: em versões anteriores (pré-1.0) do Rust, este código não teria compilado porque cada referência precisava de um lifetime explícito. Naquela época, a assinatura da função teria sido escrita assim:

fn first_word<'a>(s: &'a str) -> &'a str {

Depois de escrever muito código Rust, a equipe Rust descobriu que os programadores Rust estavam inserindo as mesmas anotações de lifetime repetidamente em situações específicas. Essas situações eram previsíveis e seguiam alguns padrões determinísticos. Os desenvolvedores programaram esses padrões no código do compilador para que o verificador de empréstimo pudesse inferir os lifetimes nessas situações e não precisasse de anotações explícitas.

Este pedaço da história do Rust é relevante porque é possível que mais padrões determinísticos surjam e sejam adicionados ao compilador. No futuro, ainda menos anotações de lifetime podem ser necessárias.

Os padrões programados na análise de referências do Rust são chamados de regras de elisão de lifetime. Estas não são regras para os programadores seguirem; são um conjunto de casos particulares que o compilador considerará, e se seu código se encaixa nesses casos, você não precisa escrever os lifetimes explicitamente.

As regras de elisão não fornecem inferência completa. Se o Rust aplicar as regras de forma determinística, mas ainda houver ambiguidade quanto aos lifetimes que as referências têm, o compilador não adivinhará qual deve ser o lifetime das referências restantes. Em vez de adivinhar, o compilador fornecerá um erro que você pode resolver adicionando as anotações de lifetime.

Lifetimes em parâmetros de função ou método são chamados de lifetimes de entrada, e lifetimes em valores de retorno são chamados de lifetimes de saída.

O compilador usa três regras para descobrir os lifetimes das referências quando não há anotações explícitas. A primeira regra se aplica aos lifetimes de entrada, e a segunda e a terceira regras se aplicam aos lifetimes de saída. Se o compilador chegar ao final das três regras e ainda houver referências para as quais não consegue descobrir os lifetimes, o compilador parará com um erro. Essas regras se aplicam a definições fn, bem como a blocos impl.

A primeira regra é que o compilador atribui um parâmetro de lifetime a cada parâmetro que é uma referência. Em outras palavras, uma função com um parâmetro recebe um parâmetro de lifetime: fn foo<'a>(x: &'a i32); uma função com dois parâmetros recebe dois parâmetros de lifetime separados: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); e assim por diante.

A segunda regra é que, se houver exatamente um parâmetro de lifetime de entrada, esse lifetime é atribuído a todos os parâmetros de lifetime de saída: fn foo<'a>(x: &'a i32) -> &'a i32.

A terceira regra é que, se houver vários parâmetros de lifetime de entrada, mas um deles for &self ou &mut self porque este é um método, o lifetime de self é atribuído a todos os parâmetros de lifetime de saída. Esta terceira regra torna os métodos muito mais agradáveis de ler e escrever porque menos símbolos são necessários.

Vamos fingir que somos o compilador. Aplicaremos essas regras para descobrir os lifetimes das referências na assinatura da função first_word na Listagem 10-25. A assinatura começa sem nenhum lifetime associado às referências:

fn first_word(s: &str) -> &str {

Então, o compilador aplica a primeira regra, que especifica que cada parâmetro recebe seu próprio lifetime. Vamos chamá-lo de 'a como de costume, então agora a assinatura é esta:

fn first_word<'a>(s: &'a str) -> &str {

A segunda regra se aplica porque há exatamente um lifetime de entrada. A segunda regra especifica que o lifetime do único parâmetro de entrada é atribuído ao lifetime de saída, então a assinatura agora é esta:

fn first_word<'a>(s: &'a str) -> &'a str {

Agora, todas as referências nesta assinatura de função têm lifetimes, e o compilador pode continuar sua análise sem que o programador precise anotar os lifetimes nesta assinatura de função.

Vamos analisar outro exemplo, desta vez usando a função longest que não tinha parâmetros de lifetime quando começamos a trabalhar com ela na Listagem 10-20:

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

Vamos aplicar a primeira regra: cada parâmetro recebe seu próprio lifetime. Desta vez, temos dois parâmetros em vez de um, então temos dois lifetimes:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

Você pode ver que a segunda regra não se aplica porque há mais de um lifetime de entrada. A terceira regra também não se aplica, porque longest é uma função em vez de um método, então nenhum dos parâmetros é self. Depois de trabalhar com todas as três regras, ainda não descobrimos qual é o lifetime do tipo de retorno. É por isso que recebemos um erro ao tentar compilar o código na Listagem 10-20: o compilador trabalhou com as regras de elisão de lifetime, mas ainda não conseguiu descobrir todos os lifetimes das referências na assinatura.

Como a terceira regra realmente só se aplica em assinaturas de métodos, analisaremos os lifetimes nesse contexto a seguir para ver por que a terceira regra significa que não precisamos anotar lifetimes em assinaturas de métodos com muita frequência.

Anotações de Lifetime em Definições de Métodos

Quando implementamos métodos em uma struct com lifetimes, usamos a mesma sintaxe que a dos parâmetros de tipo genéricos mostrados na Listagem 10-11. Onde declaramos e usamos os parâmetros de lifetime depende se eles estão relacionados aos campos da struct ou aos parâmetros e valores de retorno do método.

Os nomes de lifetime para campos de struct sempre precisam ser declarados após a palavra-chave impl e, em seguida, usados após o nome da struct, porque esses lifetimes fazem parte do tipo da struct.

Nas assinaturas de métodos dentro do bloco impl, as referências podem estar vinculadas ao lifetime das referências nos campos da struct, ou podem ser independentes. Além disso, as regras de elisão de lifetime geralmente fazem com que as anotações de lifetime não sejam necessárias nas assinaturas de métodos. Vamos analisar alguns exemplos usando a struct chamada ImportantExcerpt que definimos na Listagem 10-24.

Primeiro, usaremos um método chamado level cujo único parâmetro é uma referência a self e cujo valor de retorno é um i32, que não é uma referência a nada:

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

A declaração do parâmetro de lifetime após impl e seu uso após o nome do tipo são necessários, mas não somos obrigados a anotar o lifetime da referência a self por causa da primeira regra de elisão.

Aqui está um exemplo onde a terceira regra de elisão de lifetime se aplica:

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

Existem dois lifetimes de entrada, então o Rust aplica a primeira regra de elisão de lifetime e dá a &self e announcement seus próprios lifetimes. Então, como um dos parâmetros é &self, o tipo de retorno recebe o lifetime de &self, e todos os lifetimes foram contabilizados.

O Lifetime Static

Um lifetime especial que precisamos discutir é 'static, que denota que a referência afetada pode viver durante toda a duração do programa. Todos os literais de string têm o lifetime 'static, que podemos anotar da seguinte forma:

let s: &'static str = "I have a static lifetime.";

O texto desta string é armazenado diretamente no binário do programa, que está sempre disponível. Portanto, o lifetime de todos os literais de string é 'static.

Você pode ver sugestões para usar o lifetime 'static em mensagens de erro. Mas antes de especificar 'static como o lifetime para uma referência, pense se a referência que você tem realmente vive durante todo o lifetime do seu programa ou não, e se você quer que ela viva. Na maioria das vezes, uma mensagem de erro sugerindo o lifetime 'static resulta da tentativa de criar uma referência pendente ou uma incompatibilidade dos lifetimes disponíveis. Nesses casos, a solução é corrigir esses problemas, não especificar o lifetime 'static.

Parâmetros de Tipo Genéricos, Trait Bounds e Lifetimes Juntos

Vamos dar uma olhada breve na sintaxe de como especificar parâmetros de tipo genéricos, trait bounds e lifetimes tudo em uma função!

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {ann}");
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Esta é a função longest da Listagem 10-21 que retorna a maior de duas string slices. Mas agora ela tem um parâmetro extra chamado ann do tipo genérico T, que pode ser preenchido por qualquer tipo que implemente o trait Display, conforme especificado pela cláusula where. Este parâmetro extra será impresso usando {}, e é por isso que o trait bound Display é necessário. Como os lifetimes são um tipo de genérico, as declarações do parâmetro de lifetime 'a e do parâmetro de tipo genérico T vão na mesma lista dentro dos colchetes angulares após o nome da função.

Resumo

Parabéns! Você concluiu o laboratório Validando Referências com Lifetimes. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.