Reutilize estruturas de dados com genéricos

RustBeginner
Pratique Agora

Introdução

Tipos concretos são úteis quando os dados têm um formato fixo, mas repetir tipos quase idênticos para números e texto torna o programa mais difícil de manter. Você já usou tipos genéricos padrão, como Option<u32>, Vec<String>, HashMap<String, u32> e Result<T, E>. Neste Lab, você definirá seu próprio tipo genérico. Os genéricos do Rust permitem que uma única definição funcione com vários tipos concretos, enquanto o compilador continua verificando cada uso.

Neste lab, você tornará genérica uma estrutura Pair já preparada e, depois, generalizará um pequeno auxiliar que recebe uma referência. O programa mais longo e os dados de exemplo já estão escritos, portanto seu trabalho ficará concentrado nas poucas posições de tipo que permitem a reutilização.

Torne a estrutura Pair genérica

Nesta etapa, você substituirá o tipo fixo i32 da estrutura por um parâmetro de tipo, para que a mesma definição possa armazenar pontuações ou nomes de rotas.

Entre no pacote Cargo preparado:

cd /home/labex/project/pair-demo

Inspecione o arquivo-fonte curto. O comando sed -n exibe o intervalo de linhas solicitado sem alterar o arquivo:

sed -n '1,120p' src/main.rs

O valor scores contém inteiros, enquanto routes contém valores String próprios. A definição atual de Pair fixa os dois campos como i32, portanto não consegue representar os dois valores. O atributo #[derive(Debug)] já preparado geraria a formatação de depuração para o tipo, mas o programa visível não usa essa formatação; ela não faz parte da tarefa sobre genéricos.

Um tipo genérico introduz um marcador, como T, dentro de sinais de menor e maior. Cada Pair criado ainda tem um tipo concreto: o compilador infere Pair<i32> para scores e Pair<String> para routes.

Abra o código-fonte no Nano:

nano src/main.rs

Faça estas três pequenas alterações perto do início do arquivo:

  • Altere struct Pair { para struct Pair<T> {.
  • Altere o tipo de left de i32 para T.
  • Altere o tipo de right de i32 para T.

A estrutura completa ficará assim:

struct Pair<T> {
    left: T,
    right: T,
}

Aqui, os dois campos usam o mesmo T, portanto um par não pode misturar acidentalmente tipos de campos não relacionados. Salve com Ctrl+O, pressione Enter e saia com Ctrl+X.

Peça ao Cargo para verificar os tipos do pacote:

cargo check

Uma linha terminada em Finished significa que o compilador aceitou Pair<i32> e Pair<String>. Agora execute o programa:

cargo run --quiet
scores: 82 and 91
routes: north and east
first score: 82

As duas primeiras linhas comprovam que uma única definição de estrutura agora armazena dois tipos concretos diferentes. O auxiliar first existente ainda aceita apenas Pair<i32>, e você o generalizará em seguida.

Generalize um auxiliar que recebe uma referência

Nesta etapa, você dará à função first seu próprio parâmetro de tipo, para que ela possa receber uma referência a qualquer Pair<T> e retornar uma referência ao primeiro valor.

Inspecione o auxiliar atual perto do início do arquivo:

sed -n '1,30p' src/main.rs

A entrada Pair<i32> e a saída &i32 tornam a função específica para pontuações. Uma função genérica declara <T> depois do nome e, em seguida, usa esse mesmo T onde aparecem os tipos de entrada e saída relacionados.

Abra o arquivo:

nano src/main.rs

Altere somente a declaração da função, de:

fn first(pair: &Pair<i32>) -> &i32 {

para:

fn first<T>(pair: &Pair<T>) -> &T {

A função recebe uma referência ao par e retorna uma referência a um campo; portanto, ela não move nem copia o valor armazenado. O Rust consegue inferir T a partir de cada argumento; quem chama a função não precisa escrever o tipo explicitamente.

Em seguida, substitua // FIRST_ROUTE_REPORT por uma chamada que comprove que o auxiliar também funciona com texto:

    println!("first route: {}", first(&routes));

Salve e saia do Nano. Depois, verifique os tipos e execute o programa completo:

cargo check
cargo run --quiet
scores: 82 and 91
routes: north and east
first score: 82
first route: north

As duas linhas first vêm da mesma função genérica. O Rust infere T = i32 para a chamada com a pontuação e T = String para a chamada com a rota, e então verifica cada chamada usando esse tipo concreto. A reutilização não elimina a verificação estática de tipos.

Resumo

Você substituiu um par concreto por Pair<T>, usou essa estrutura com inteiros e strings e generalizou um auxiliar que recebe uma referência sem introduzir traits nem duplicar implementações.