Mostrando postagens com marcador Rust. Mostrar todas as postagens
Mostrando postagens com marcador Rust. Mostrar todas as postagens

sábado, 17 de abril de 2021

Traços em Rust: Definindo Comportamento Compartilhado

Um traço informa ao compilador Rust sobre a funcionalidade que um determinado tipo possui e pode compartilhar com outros tipos. Podemos usar traços para definir o comportamento compartilhado de uma forma abstrata. Podemos usar limites de traços para especificar que um genérico pode ser qualquer tipo que tenha determinado comportamento.

Nota: Traços são semelhantes a um recurso freqüentemente chamado de interfaces em outras linguagens, embora com algumas diferenças.

Definindo um traço

O comportamento de um tipo consiste nos métodos que podemos chamar a esse tipo. Tipos diferentes compartilham o mesmo comportamento se pudermos chamar os mesmos métodos em todos esses tipos. As definições de traços são uma forma de agrupar assinaturas de método para definir um conjunto de comportamentos necessários para realizar algum propósito.

Por exemplo, digamos que temos várias estruturas que contêm vários tipos e quantidades de texto: uma estrutura NovosArtigos que contém uma notícia arquivada em um local específico e um Tweet que pode ter no máximo 280 caracteres junto com metadados que indicam se foi um novo tweet, um retuíte ou uma resposta a outro tweet.

Queremos fazer uma biblioteca agregadora de mídia que pode exibir resumos de dados que podem ser armazenados em uma instância NovosArtigos ou Tweet. Para fazer isso, precisamos de um resumo de cada tipo e precisamos solicitar esse resumo chamando um método resumir em uma instância. A Listagem 10-12 mostra a definição de um traço Resumo que expressa esse comportamento.

Nome do arquivo: src/lib.rs

pub trait Resumo {
    fn resumir(&self) -> String;
}

Listagem 10-12: Um traço Resumo que consiste no comportamento fornecido por um método resumir

Aqui, declaramos uma traço usando a palavra-chave trait e depois o nome do traço, que é Resumo neste caso. Dentro das chaves, declaramos as assinaturas do método que descrevem os comportamentos dos tipos que implementam esse traço, que neste caso é fn resumir(&self) -> String.

Após a assinatura do método, em vez de fornecer uma implementação entre chaves, usamos um ponto-e-vírgula. Cada tipo que implementa esse traço deve fornecer seu próprio comportamento personalizado para o corpo do método. O compilador fará com que qualquer tipo que tenha o traço Resumo tenha o método resumir definido exatamente com esta assinatura.

Um traço pode ter vários métodos em seu corpo: as assinaturas do método são listadas uma por linha e cada linha termina em um ponto-e-vírgula.

Implementando um traço em um tipo

Agora que definimos o comportamento desejado usando o traço Resumo, podemos implementá-lo nos tipos em nosso agregador de mídia. A Listagem 10-13 mostra uma implementação do traço Resumo na estrutura NovosArtigos que usa o título, o autor e o local para criar o valor de retorno resumir. Para a estrutura Tweet, definimos resumir como o nome de usuário seguido por todo o texto do tweet, supondo que o conteúdo do tweet já esteja limitado a 280 caracteres.

Nome do arquivo: src/lib.rs

pub trait Resumo {
    fn resumir(&self) -> String;
}

pub struct NovosArtigos {
    pub titulo: String,
    pub localizacao: String,
    pub autor: String,
    pub conteudo: String,
}

impl Resumo for NovosArtigos {
    fn resumir(&self) -> String {
        format!("{}, by {} ({})", self.titulo, self.autor, self.localizacao)
    }
}

pub struct Tweet {
    pub nomeusuario: String,
    pub conteudo: String,
    pub resposta: bool,
    pub retweet: bool,
}

impl Resumo for Tweet {
    fn resumir(&self) -> String {
        format!("{}: {}", self.nomeusuario, self.conteudo)
    }
}

Listagem 10-13: Implementando o traço Resumo nos tipos NovosArtigos e Tweet

Implementar uma traço em um tipo é semelhante a implementar métodos regulares. A diferença é que depois de impl colocamos o nome do traço que queremos implementar, usamos a palavra-chave for e especificamos o nome do tipo para o qual queremos implementar o traço. Dentro do bloco impl, colocamos as assinaturas de método que a definição de traço definiu. Em vez de adicionar um ponto-e-vírgula após cada assinatura, usamos chaves e preenchemos o corpo do método com o comportamento específico que desejamos que os métodos do traço tenham para o tipo específico.

Depois de implementar o traço, podemos chamar os métodos em instâncias de NovosArtigos e Tweet da mesma forma que chamamos métodos regulares, como este:

use chapter10::{self, Resumo, Tweet};

fn main() {
    let tweet = Tweet {
        nomeusuario: String::from("ebooks_de_cavalos"),
        conteudo: String::from(
            "claro, como você provavelmente já sabe, as pessoas",
        ),
        resposta: false,
        retweet: false,
    };

    println!("1 novo tweet: {}", tweet.resumir());
}

Este código é impresso 1 novo tweet: ebooks_de_cavalos: claro, como você provavelmente já sabe, as pessoas.

Observe que, como definimos o traço Resumo e os tipos NovosArtigos e Tweet no mesmo arquivo lib.rs na Listagem 10-13, eles estão todos no mesmo escopo. Digamos que este arquivo lib.rs seja para um crate que chamamos aggregator e outra pessoa deseja usar a funcionalidade de nosso crate para implementar o traço Resumo em uma estrutura definida dentro do escopo de sua biblioteca. Eles precisariam trazer o traço para seu escopo primeiro. Eles fariam isso especificando use aggregator::Resumo;, o que lhes permitiria implementar Resumo para seu tipo. O traço Resumo também precisaria ser um traço público para que outro crate o implementasse, porque colocamos a palavra-chave pub antes trait na Listagem 10-12.

Uma restrição a ser observada com as implementações de traços é que podemos implementar um traço em um tipo apenas se o traço ou o tipo for local para nosso crate. Por exemplo, podemos implementar traços de biblioteca padrão como Display em um tipo personalizado Tweet como parte de nossa funcionalidade do crate aggregator, porque o tipo Tweet é local para nosso crate aggregator. Nós também podemos implementar Resumo em Vec<T> no nosso crate aggregator, porque o traço Resumo é local para o nosso crate aggregator.

Mas não podemos implementar traços externos em tipos externos. Por exemplo, não podemos implementar o traço Display em nosso Vec<T> dentro de nosso crate aggregator, porque Display e Vec<T> são definidos na biblioteca padrão e não são locais em nosso crate aggregator. Essa restrição é parte de uma propriedade dos programas chamada coerência e, mais especificamente, a regra órfã, assim chamada porque o tipo pai não está presente. Esta regra garante que o código de outras pessoas não possa quebrar seu código e vice-versa. Sem a regra, dois crates poderiam implementar o mesmo traço para o mesmo tipo, e Rust não saberia qual implementação usar.

Implementações padrão

Às vezes, é útil ter um comportamento padrão para alguns ou todos os métodos em um traço, em vez de exigir implementações para todos os métodos em cada tipo. Então, conforme implementamos o traço em um tipo específico, podemos manter ou substituir o comportamento padrão de cada método.

A Listagem 10-14 mostra como especificar uma string padrão para o método resumir do traço Resumo em vez de apenas definir a assinatura do método, como fizemos na Listagem 10-12.

Nome do arquivo: src/lib.rs

pub trait Resumo {
    fn resumir(&self) -> String {
        String::from("(Ler mais...)")
    }
}

pub struct NovosArtigos {
    pub titulo: String,
    pub localizacao: String,
    pub autor: String,
    pub conteudo: String,
}

impl Resumo for NovosArtigos {}

pub struct Tweet {
    pub nomeusuario: String,
    pub conteudo: String,
    pub resposta: bool,
    pub retweet: bool,
}

impl Resumo for Tweet {
    fn resumir(&self) -> String {
        format!("{}: {}", self.nomeusuario, self.conteudo)
    }
}

Listagem 10-14: Definição de um traço Resumo com uma implementação padrão do método resumir

Para usar uma implementação padrão para resumir as instâncias NovosArtigos em vez de definir uma implementação customizada, especificamos um bloco impl vazio com impl Resumo for NovosArtigos {}.

Mesmo que não estejamos mais definindo o método resumir em NovosArtigos diretamente, fornecemos uma implementação padrão e especificamos que NovosArtigos implementa o traço Resumo. Como resultado, ainda podemos chamar o método resumir em uma instância de NovosArtigos, assim:

use chapter10::{self, NovosArtigos, Resumo};

fn main() {
    let article = NovosArtigos {
        titulo: String::from("Os pinguins vencem o campeonato da Stanley Cup!"),
        localizacao: String::from("Pittsburgh, PA, USA"),
        autor: String::from("Iceburgh"),
        conteudo: String::from(
            "Os Penguins de Pittsburgh, mais uma vez, são o melhor \
             time de hockey da NHL.",
        ),
    };

    println!("Novo artigo disponível! {}", article.resumir());
}

Este código imprime Novo artigo disponível! (Ler mais...).

Criando uma implementação padrão para resumir não nos obrigam a mudar alguma coisa sobre a implementação de Resumo em Tweet na Listagem 10-13. O motivo é que a sintaxe para substituir uma implementação padrão é a mesma que a sintaxe para implementar um método de traço que não tem uma implementação padrão.

Implementações padrão podem chamar outros métodos na mesma traço, mesmo se esses outros métodos não tiverem uma implementação padrão. Dessa forma, uma traço pode fornecer muitas funcionalidades úteis e requer apenas que os implementadores especifiquem uma pequena parte dela. Por exemplo, podemos definir o traço Resumo para ter um método resumir_autor cuja implementação é necessária e, em seguida, definir um método resumir que tem uma implementação padrão que chama o método resumir_autor:

pub trait Resumo {
    fn resumir_autor(&self) -> String;

    fn resumir(&self) -> String {
        format!("(Leia mais de {}...)", self.resumir_autor())
    }
}

pub struct Tweet {
    pub nomeusuario: String,
    pub conteudo: String,
    pub resposta: bool,
    pub retweet: bool,
}

impl Resumo for Tweet {
    fn resumir_autor(&self) -> String {
        format!("@{}", self.nomeusuario)
    }
}

Para usar esta versão de Resumo, só precisamos definir resumir_autor quando implementamos o traço em um tipo:

pub trait Resumo {
    fn resumir_autor(&self) -> String;

    fn resumir(&self) -> String {
        format!("(Leia mais de {}...)", self.resumir_autor())
    }
}

pub struct Tweet {
    pub nomeusuario: String,
    pub conteudo: String,
    pub resposta: bool,
    pub retweet: bool,
}

impl Resumo for Tweet {
    fn resumir_autor(&self) -> String {
        format!("@{}", self.nomeusuario)
    }
}

Depois de definirmos resumir_autor, podemos chamar a instância resumir da estrutura Tweet, e a implementação padrão de resumir chamará a definição resumir_autor que fornecemos. Como implementamos resumir_autor, o traço Resumo nos deu o comportamento do método resumir sem exigir que escrevêssemos mais nenhum código.

use chapter10::{self, Resumo, Tweet};

fn main() {
    let tweet = Tweet {
        nomeusuario: String::from("ebooks_de_cavalos"),
        conteudo: String::from(
            "claro, como você provavelmente já sabe, as pessoas",
        ),
        resposta: false,
        retweet: false,
    };

    println!("1 novo tweet: {}", tweet.resumir());
}

Este código imprime 1 novo tweet: (Leia mais de @ebooks_de_cavalos...).

Observe que não é possível chamar a implementação padrão de uma implementação de substituição do mesmo método.

Traços como parâmetros

Agora que você sabe como definir e implementar características, podemos explorar como usar características para definir funções que aceitam muitos tipos diferentes.

Por exemplo, na Listagem 10-13, implementamos o traço Resumo nos tipos NovosArtigos e Tweet. Podemos definir uma função notificar que chama o método resumir em seu parâmetro item, que é de algum tipo que implementa o traço Resumo. Para fazer isso, podemos usar a sintaxe impl Trait, assim:

pub trait Resumo {
    fn resumir(&self) -> String;
}

pub struct NovosArtigos {
    pub titulo: String,
    pub localizacao: String,
    pub autor: String,
    pub conteudo: String,
}

impl Resumo for NovosArtigos {
    fn resumir(&self) -> String {
        format!("{}, by {} ({})", self.titulo, self.autor, self.localizacao)
    }
}

pub struct Tweet {
    pub nomeusuario: String,
    pub conteudo: String,
    pub resposta: bool,
    pub retweet: bool,
}

impl Resumo for Tweet {
    fn resumir(&self) -> String {
        format!("{}: {}", self.nomeusuario, self.conteudo)
    }
}

pub fn notificar(item: &impl Resumo) {
    println!("Últimas notícias! {}", item.resumir());
}

Em vez de um tipo concreto para o parâmetro item, especificamos a palavra-chave impl e o nome do traço. Este parâmetro aceita qualquer tipo que implemente o traço especificado. No corpo de notificar, podemos chamar qualquer método item proveniente do traço Resumo, como resumir. Podemos chamar notificar e passar em qualquer instância de NovosArtigos ou Tweet. O código que chama a função com qualquer outro tipo, como a String ou um i32, não será compilado porque esses tipos não implementam Resumo.

Sintaxe de limite de traço

A sintaxe impl Trait funciona para casos simples, mas, na verdade, é um açúcar de sintaxe para uma forma mais longa, que é chamada de limite de traço; Se parece com isso:

pub fn notificar<T: Resumo>(item: &T) {
    println!("Últimas notícias! {}", item.resumir());
}

Esta forma mais longa é equivalente ao exemplo da seção anterior, mas é mais detalhada. Colocamos limites de traço com a declaração do parâmetro de tipo genérico depois de dois pontos e entre colchetes angulares.

A sintaxe impl Trait é conveniente e torna o código mais conciso em casos simples. A sintaxe associada ao traço pode expressar mais complexidade em outros casos. Por exemplo, podemos ter dois parâmetros que implementam Resumo. O uso da sintaxe impl Trait é parecido com o seguinte:

pub fn notificar(item1: &impl Resumo, item2: &impl Resumo) {

Se quiséssemos que essa função permitisse que item1 e item2 tivesse tipos diferentes, o uso de impl Trait seria apropriado (desde que ambos os tipos implementem Resumo). Se quisermos forçar os dois parâmetros a terem o mesmo tipo, isso só é possível expressar usando um limite de traço, como este:

pub fn notificar<T: Resumo>(item1: &T, item2: &T) {

O tipo genérico T especificado como o tipo dos parâmetros item1 e item2 restringe a função de modo que o tipo concreto do valor passado como um argumento para item1 e item2 deve ser o mesmo.

Especificando vários limites de traços com a sintaxe +

Também podemos especificar mais de um limite de traço. Digamos que quiséssemos usar notificar para formatação de exibição de item, bem como o método resumir: especificamos na definição de notificar que item deve implementar Display e Resumo. Podemos fazer isso usando a sintaxe +:

pub fn notificar(item: &(impl Resumo + Display)) {

A sintaxe + também é válida com limites de traço em tipos genéricos:

pub fn notificar<T: Resumo + Display>(item: &T) {

Com os dois limites de traço especificados, o corpo de notificar pode chamar resumir e usar {} para formatar item.

Limites de traço mais claros com a cláusulas where

Usar muitos limites de características tem suas desvantagens. Cada genérico tem seus próprios limites de traço, portanto funções com vários parâmetros de tipo genérico podem conter muitas informações de limite de traço entre o nome da função e sua lista de parâmetros, tornando a assinatura da função difícil de ler. Por esse motivo, Rust tem uma sintaxe alternativa para especificar limites de características dentro de uma cláusula where após a assinatura da função. Então, em vez de escrever isso:

fn alguma_funcao<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

podemos usar uma cláusula where, como esta:

fn alguma_funcao<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{

A assinatura dessa função é menos confusa: o nome da função, a lista de parâmetros e o tipo de retorno estão próximos, semelhante a uma função sem muitos limites de características.

Retornando Tipos que Implementam Traços

Também podemos usar a sintaxe impl Trait na posição de retorno para retornar um valor de algum tipo que implementa uma traço, conforme mostrado aqui:

pub trait Resumo {
    fn resumir(&self) -> String;
}

pub struct NovosArtigos {
    pub titulo: String,
    pub localizacao: String,
    pub autor: String,
    pub conteudo: String,
}

impl Resumo for NovosArtigos {
    fn resumir(&self) -> String {
        format!("{}, by {} ({})", self.titulo, self.autor, self.localizacao)
    }
}

pub struct Tweet {
    pub nomeusuario: String,
    pub conteudo: String,
    pub resposta: bool,
    pub retweet: bool,
}

impl Resumo for Tweet {
    fn resumir(&self) -> String {
        format!("{}: {}", self.nomeusuario, self.conteudo)
    }
}

fn retorna_resumivel() -> impl Resumo {
    Tweet {
        nomeusuario: String::from("ebooks_de_cavalos"),
        conteudo: String::from(
            "claro, como você provavelmente já sabe, as pessoas",
        ),
        resposta: false,
        retweet: false,
    }
}

Ao usar impl Resumo para o tipo de retorno, especificamos que a função retorna_resumivel retorna algum tipo que implementa o traço Resumo sem nomear o tipo concreto. Nesse caso, retorna_resumivel retorna um Tweet, mas o código que chama esta função não sabe disso.

A capacidade de retornar um tipo que é especificado apenas pela traço que ele implementa é especialmente útil no contexto de fechamentos e iteradores, que abordamos no Capítulo 13. Fechamentos e iteradores criam tipos que apenas o compilador conhece ou tipos que são muito longos para especificamos. A impl Traitsintaxe permite que você especifique concisamente que uma função retorna algum tipo que implementa a Iteratortraço sem a necessidade de escrever um tipo muito longo.

No entanto, você só pode usar impl Traitse estiver retornando um único tipo. Por exemplo, este código que retorna a NovosArtigosou a Tweetcom o tipo de retorno especificado como impl Resumonão funcionaria:

Esse código não compila Esse código não compila.

pub trait Resumo {
    fn resumir(&self) -> String;
}

pub struct NovosArtigos {
    pub titulo: String,
    pub localizacao: String,
    pub autor: String,
    pub conteudo: String,
}

impl Resumo for NovosArtigos {
    fn resumir(&self) -> String {
        format!("{}, by {} ({})", self.titulo, self.autor, self.localizacao)
    }
}

pub struct Tweet {
    pub nomeusuario: String,
    pub conteudo: String,
    pub resposta: bool,
    pub retweet: bool,
}

impl Resumo for Tweet {
    fn resumir(&self) -> String {
        format!("{}: {}", self.nomeusuario, self.conteudo)
    }
}

fn retorna_resumivel(switch: bool) -> impl Resumo {
    if switch {
        NovosArtigos {
            titulo: String::from(
                "Os pinguins vencem o campeonato da Stanley Cup!",
            ),
            localizacao: String::from("Pittsburgh, PA, USA"),
            autor: String::from("Iceburgh"),
            conteudo: String::from(
                "Os Penguins de Pittsburgh, mais uma vez, são o melhor \
                 time de hockey da NHL.",
            ),
        }
    } else {
        Tweet {
            nomeusuario: String::from("ebooks_de_cavalos"),
            conteudo: String::from(
                "claro, como você provavelmente já sabe, as pessoas",
            ),
            resposta: false,
            retweet: false,
        }
    }
}

Retornar um NovosArtigos ou um Tweet não é permitido devido a restrições sobre como a sintaxe impl Trait é implementada no compilador. Abordaremos como escrever uma função com esse comportamento na seção “Usando objetos de traço que permitem valores de tipos diferentes” do Capítulo 17.

Corrigindo a função maior com limites de traço

Agora que você sabe como especificar o comportamento que deseja usar usando os limites do parâmetro de tipo genérico, vamos retornar à Listagem 10-5 para corrigir a definição da função maior que usa um parâmetro de tipo genérico! Da última vez que tentamos executar esse código, recebemos este erro:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
 --> src/main.rs:5:17
  |
5 |         if item > maior {
  |            ---- ^ ------- &T
  |            |
  |            &T
  |
help: consider restricting type parameter `T`
  |
1 | fn maior<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
  |             ^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10`

To learn more, run the command again with --verbose.

No corpo da função maior, queríamos comparar dois valores de tipo T usando o operador maior que (>). Como esse operador é definido como um método padrão no traço da biblioteca padrão std::cmp::PartialOrd, precisamos especificar PartialOrd nos limites do traço para T que a função maior possa funcionar em fatias de qualquer tipo que possamos comparar. Não precisamos trazer PartialOrd para o escopo porque está no prelúdio. Altere a assinatura de maior para ficar assim:

fn maior<T: PartialOrd>(list: &[T]) -> T {
    let mut maior = list[0];

    for &item in list {
        if item > maior {
            maior = item;
        }
    }

    maior
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = maior(&number_list);
    println!("O maior número é {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = maior(&char_list);
    println!("O maior char é {}", result);
}

Desta vez, quando compilamos o código, obtemos um conjunto diferente de erros:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0508]: cannot move out of type `[T]`, a non-copy slice
 --> src/main.rs:2:23
  |
2 |     let mut maior = list[0];
  |                       ^^^^^^^
  |                       |
  |                       cannot move out of here
  |                       move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait
  |                       help: consider borrowing here: `&list[0]`

error[E0507]: cannot move out of a shared reference
 --> src/main.rs:4:18
  |
4 |     for &item in list {
  |         -----    ^^^^
  |         ||
  |         |data moved here
  |         |move occurs because `item` has type `T`, which does not implement the `Copy` trait
  |         help: consider removing the `&`: `item`

error: aborting due to 2 previous errors

Some errors have detailed explanations: E0507, E0508.
For more information about an error, try `rustc --explain E0507`.
error: could not compile `chapter10`

To learn more, run the command again with --verbose.

A linha chave neste erro é cannot move out of type [T], a non-copy slice. Com nossas versões não genéricas da função maior, estávamos apenas tentando encontrar o maior i32 ou char. Conforme discutido na seção “Dados somente de pilha: cópia” no Capítulo 4, tipos como i32 e char que têm um tamanho conhecido podem ser armazenados na pilha, para que implementem o traço Copy. Mas quando tornamos a função maior genérica, tornou-se possível para o parâmetro list ter tipos que não implementam o traço Copy. Consequentemente, não poderíamos mover o valor para fora de list[0] e para dentro da variável maior, resultando neste erro.

Para chamar esse código apenas com os tipos que implementam o traço Copy, podemos adicionar Copy aos limites do traço T! A Listagem 10-15 mostra o código completo de uma função maior genérica que será compilada, desde que os tipos dos valores na fatia que passamos para a função implementem os traços PartialOrd e Copy, como i32 e char.

Nome do arquivo: src/main.rs

fn maior<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut maior = list[0];

    for &item in list {
        if item > maior {
            maior = item;
        }
    }

    maior
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = maior(&number_list);
    println!("O maior número é {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = maior(&char_list);
    println!("O maior char é {}", result);
}

Listagem 10-15: Uma definição de trabalho da função maior que funciona em qualquer tipo genérico que implementa os traços PartialOrd e Copy

Se não quisermos restringir a função maior aos tipos que implementam o traço Copy, poderíamos especificar que T tem o traço limitada em Clone em vez de Copy. Então, poderíamos clonar cada valor na fatia quando quisermos que a função maior tenha propriedade. Usar a função clone significa que estamos potencialmente fazendo mais alocações de heap no caso de tipos que possuem dados de heap String, e as alocações de heap podem ser lentas se estivermos trabalhando com grandes quantidades de dados.

Outra maneira que podemos implementar maior é a função retornar uma referência a um valor T na fatia. Se alterarmos o tipo de retorno para &T em vez de T, alterando assim o corpo da função para retornar uma referência, não precisaríamos dos limites de traço Clone ou Copy e poderíamos evitar alocações de heap. Tente implementar essas soluções alternativas por conta própria!

Usando Limites de Traço para Implementar Métodos Condicionalmente

Usando um traço vinculado a um bloco impl que usa parâmetros de tipo genérico, podemos implementar métodos condicionalmente para tipos que implementam os traços especificados. Por exemplo, o tipo Pair<T> na Listagem 10-16 sempre implementa a função new. Mas Pair<T> apenas implementa o método cmp_display se seu tipo interno T implementar o traço PartialOrd que permite a comparação e o traço Display que permite a impressão.

Nome do arquivo: src/lib.rs

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("O maior membro é x = {}", self.x);
        } else {
            println!("O maior membro é y = {}", self.y);
        }
    }
}

Listagem 10-16: Implementar métodos condicionalmente em um tipo genérico, dependendo dos limites do traço

Também podemos implementar condicionalmente um traço para qualquer tipo que implemente outro traço. As implementações de uma traço em qualquer tipo que satisfaça os limites do traço são chamadas de implementações de cobertura e são amplamente utilizadas na biblioteca padrão do Rust. Por exemplo, a biblioteca padrão implementa o traço ToString em qualquer tipo que implemente o traço Display. O bloco impl na biblioteca padrão é semelhante a este código:

impl<T: Display> ToString for T {
    // --recorte--
}

Como a biblioteca padrão tem essa implementação abrangente, podemos chamar o método to_string definido pelo traço ToString em qualquer tipo que implemente o traço Display. Por exemplo, podemos transformar inteiros em seus valores String correspondentes assim porque os inteiros implementam Display:


#![allow(unused)]
fn main() {
let s = 3.to_string();
}

Implementações de cobertura aparecem na documentação para a traço na seção “Implementadores”.

Traços e limites de traços nos permitem escrever código que usa parâmetros de tipo genérico para reduzir a duplicação, mas também especificar para o compilador que queremos que o tipo genérico tenha um comportamento particular. O compilador pode então usar as informações de limite de traço para verificar se todos os tipos concretos usados com nosso código fornecem o comportamento correto. Em linguagens tipadas dinamicamente, obteríamos um erro em tempo de execução se chamássemos um método em um tipo que não definisse o método. Mas o Rust move esses erros para o tempo de compilação, então somos forçados a consertar os problemas antes mesmo de nosso código ser executado. Além disso, não precisamos escrever código que verifique o comportamento em tempo de execução porque já o verificamos em tempo de compilação. Isso melhora o desempenho sem ter que abrir mão da flexibilidade dos genéricos.

Outro tipo de genérico que já usamos é chamado de vidas úteis. Em vez de garantir que um tipo tenha o comportamento que desejamos, os tempos de vida garantem que as referências sejam válidas pelo tempo que precisarmos. Vejamos como as vidas fazem isso.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

Tipos de dados genéricos em Rust

Podemos usar genéricos para criar definições para itens como assinaturas de função ou estruturas, que podemos então usar com muitos tipos de dados concretos diferentes. Vejamos primeiro como definir funções, estruturas, enums e métodos usando genéricos. Em seguida, discutiremos como os genéricos afetam o desempenho do código.

Em Definições de Função

Ao definir uma função que usa genéricos, colocamos os genéricos na assinatura da função onde normalmente especificaríamos os tipos de dados dos parâmetros e o valor de retorno. Isso torna nosso código mais flexível e fornece mais funcionalidade aos chamadores de nossa função, evitando a duplicação de código.

Continuando com nossa função maior, a Listagem 10-4 mostra duas funções que encontram o maior valor em uma fatia.

Nome do arquivo: src/main.rs

fn maior_i32(list: &[i32]) -> &i32 {
    let mut maior = &list[0];

    for item in list {
        if item > maior {
            maior = item;
        }
    }

    maior
}

fn maior_char(list: &[char]) -> &char {
    let mut maior = &list[0];

    for item in list {
        if item > maior {
            maior = item;
        }
    }

    maior
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = maior_i32(&number_list);
    println!("O maior numero é {}", result);
    assert_eq!(result, &100);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = maior_char(&char_list);
    println!("O maior char é {}", result);
    assert_eq!(result, &'y');
}

Listagem 10-4: Duas funções que diferem apenas em seus nomes e os tipos em suas assinaturas

A função maior_i32 é aquela que extraímos na Listagem 10-3 que encontra a maior i32 em uma fatia. A função maior_char encontra o maior char em uma fatia. Os corpos das funções têm o mesmo código, portanto, vamos eliminar a duplicação introduzindo um parâmetro de tipo genérico em uma única função.

Para parametrizar os tipos na nova função que definiremos, precisamos nomear o parâmetro de tipo, assim como fazemos para os parâmetros de valor de uma função. Você pode usar qualquer identificador como um nome de parâmetro de tipo. Mas usaremos T porque, por convenção, os nomes dos parâmetros no Rust são curtos, geralmente apenas uma letra, e a convenção de nomenclatura de tipo do Rust é CamelCase. Abreviação de “tipo”, T é a escolha padrão da maioria dos programadores do Rust.

Quando usamos um parâmetro no corpo da função, temos que declarar o nome do parâmetro na assinatura para que o compilador saiba o que esse nome significa. Da mesma forma, quando usamos um nome de parâmetro de tipo em uma assinatura de função, temos que declarar o nome do parâmetro de tipo antes de usá-lo. Para definir a função genérica maior, coloque as declarações de nome de tipo entre colchetes angulares, <>, entre o nome da função e a lista de parâmetros, assim:

fn maior<T>(list: &[T]) -> &T {

Lemos esta definição como: a função maior é genérica sobre algum tipo T. Essa função tem um parâmetro denominado list, que é uma fatia de valores do tipo T. A função maior retornará uma referência a um valor do mesmo tipo T.

A Listagem 10-5 mostra a definição da função maior combinada usando o tipo de dados genérico em sua assinatura. A lista também mostra como podemos chamar a função com uma fatia de valores i32 ou valores char. Observe que este código não será compilado ainda, mas vamos consertar isso mais tarde neste capítulo.

Nome do arquivo: src/main.rs

Esse código não compila Esse código não compila.

fn maior<T>(list: &[T]) -> &T {
    let mut maior = &list[0];

    for item in list {
        if item > maior {
            maior = item;
        }
    }

    maior
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = maior(&number_list);
    println!("O maior numero é {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = maior(&char_list);
    println!("O maior char é {}", result);
}

Listagem 10-5: Uma definição da função maior que usa parâmetros de tipo genérico, mas ainda não compila

Se compilarmos este código agora, obteremos este erro:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
 --> src/main.rs:5:17
  |
5 |         if item > maior {
  |            ---- ^ ------- &T
  |            |
  |            &T
  |
help: consider restricting type parameter `T`
  |
1 | fn maior<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
  |             ^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10`

To learn more, run the command again with --verbose.

A nota menciona std::cmp::PartialOrd, que é um característica. Falaremos sobre as características na próxima seção. Por enquanto, esse erro afirma que o corpo de maior não funcionará para todos os tipos possíveis que T poderiam ser. Como queremos comparar valores de tipo T no corpo, só podemos usar tipos cujos valores podem ser solicitados. Para permitir comparações, a biblioteca padrão tem a característica std::cmp::PartialOrd que você pode implementar em tipos (consulte o Apêndice C para mais informações sobre esta característica). Você aprenderá como especificar que um tipo genérico tem uma característica particular na seção “Características como parâmetros”, mas vamos primeiro explorar outras maneiras de usar parâmetros de tipo genérico.

Em Definições Struct

Também podemos definir estruturas para usar um parâmetro de tipo genérico em um ou mais campos usando a sintaxe <>. A Listagem 10-6 mostra como definir uma estrutura Point<T> para conter os valores das coordenadas x e y de qualquer tipo.

Nome do arquivo: src/main.rs

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

Listagem 10-6: Uma estrutura Point<T> que contém valores x e y do tipo T

A sintaxe para usar genéricos em definições de estrutura é semelhante à usada em definições de função. Primeiro, declaramos o nome do parâmetro de tipo entre colchetes angulares logo após o nome da estrutura. Então, podemos usar o tipo genérico na definição de estrutura, onde, de outra forma, especificaríamos tipos de dados concretos.

Observe que, como nós usamos apenas um tipo genérico para definir Point<T>, esta definição diz que a estrutura Point<T> é genérica sobre algum tipo T, e os campos x e y são ambos do mesmo tipo, qualquer que seja o tipo. Se criarmos uma instância de Point<T> que possui valores de tipos diferentes, como na Listagem 10-7, nosso código não será compilado.

Nome do arquivo: src/main.rs

Esse código não compila Esse código não compila.

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let wont_work = Point { x: 5, y: 4.0 };
}

Listagem 10-7: Os campos x e y devem ser do mesmo tipo porque ambos têm o mesmo tipo de dados genérico T.

Neste exemplo, quando atribuímos o valor inteiro 5 a x, informamos ao compilador que o tipo genérico T será um inteiro para esta instância de Point<T>. Então, quando especificarmos 4.0 para y, que definimos como sendo do mesmo tipo x, obteremos um erro de incompatibilidade de tipo como este:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
 --> src/main.rs:7:38
  |
7 |     let wont_work = Point { x: 5, y: 4.0 };
  |                                      ^^^ expected integer, found floating-point number

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10`

To learn more, run the command again with --verbose.

Para definir uma estrutura Point onde x e y são genéricos, mas podem ter tipos diferentes, podemos usar vários parâmetros de tipo genérico. Por exemplo, na Listagem 10-8, podemos alterar a definição de Point para ser genérico sobre os tipos T e U onde x é do tipo T e y é do tipo U.

Nome do arquivo: src/main.rs

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

Listagem 10-8: Um genérico Point<T, U> sobre dois tipos, de modo que x e y podem ser valores de tipos diferentes

Agora todas as instâncias de Point mostrado são permitidas! Você pode usar quantos parâmetros de tipo genérico quiser em uma definição, mas usar mais do que alguns torna seu código difícil de ler. Quando você precisa de muitos tipos genéricos em seu código, isso pode indicar que seu código precisa ser reestruturado em partes menores.

Em Definições Enum

Como fizemos com structs, podemos definir enums para conter tipos de dados genéricos em suas variantes. Vamos dar uma outra olhada no enum Option<T> que a biblioteca padrão fornece, que usamos no Capítulo 6:


#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}
}

Esta definição agora deve fazer mais sentido para você. Como você pode ver, Option<T> é um enum que é genérico sobre o tipo T e tem duas variantes:, Some que contém um valor de tipo T e uma variante None que não contém nenhum valor. Usando o enum Option<T>, podemos expressar o conceito abstrato de ter um valor opcional e, por Option<T> ser genérico, podemos usar essa abstração independentemente do tipo do valor opcional.

Enums também podem usar vários tipos genéricos. A definição do enum Result que usamos no Capítulo 9 é um exemplo:


#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

O enum Result é genérico em dois tipos, T e E, e tem duas variantes: Ok que contém um valor de tipo T e Err, que contém um valor de tipo E. Essa definição torna conveniente usar o enum Result em qualquer lugar em que tenhamos uma operação que pode ser bem-sucedida (retornar um valor de algum tipo T) ou se falhar (retornar um erro de algum tipo E). Na verdade, é isso que usamos para abrir um arquivo na Listagem 9-3, onde T foi preenchido com o tipo std::fs::File quando o arquivo foi aberto com sucesso e E foi preenchido com o tipo std::io::Error quando houve problemas ao abrir o arquivo.

Ao reconhecer situações em seu código com várias definições de struct ou enum que diferem apenas nos tipos dos valores que contêm, você pode evitar a duplicação usando tipos genéricos.

Em Definições de Método

Podemos implementar métodos em structs e enums (como fizemos no Capítulo 5) e usar tipos genéricos em suas definições também. A Listagem 10-9 mostra a estrutura Point<T> que definimos na Listagem 10-6 com um método denominado x implementado nela.

Nome do arquivo: src/main.rs

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

Listagem 10-9: Implementando um método nomeado x na estrutura Point<T> que retornará uma referência ao campo x do tipo T

Aqui, definimos um método denominado x em Point<T> que retorna uma referência aos dados no campo x.

Observe que temos que declarar T logo depois de impl para que possamos usá-lo para especificar que estamos implementando métodos no tipo Point<T>. Ao declarar T como um tipo genérico depois de impl, Rust pode identificar que o tipo entre os colchetes angulares em Point é um tipo genérico em vez de um tipo concreto.

Poderíamos, por exemplo, implementar métodos apenas nas instâncias de Point<f32>, em vez das instâncias Point<T> com qualquer tipo genérico. Na Listagem 10-10, usamos o tipo concreto f32, o que significa que não declaramos nenhum tipo depois de impl.

Nome do arquivo: src/main.rs

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

impl Point<f32> {
    fn distancia_da_origen(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

Listagem 10-10: Um bloco impl que se aplica apenas a uma estrutura com um tipo específico de concreto para o parâmetro de tipo genérico T

Este código significa que o tipo Point<f32> terá um método denominado distancia_da_origen e outras instâncias de Point<T> onde T não é do tipo f32 não terão esse método definido. O método mede o quão longe nosso ponto está do ponto nas coordenadas (0,0, 0,0) e usa operações matemáticas que estão disponíveis apenas para tipos de ponto flutuante.

Os parâmetros de tipo genérico em uma definição de estrutura nem sempre são os mesmos que você usa nas assinaturas de método dessa estrutura. Por exemplo, a Listagem 10-11 define o método mixup na estrutura Point<T, U> da Listagem 10-8. O método recebe outro Point como parâmetro, que pode ter tipos diferentes do self Point que estamos chamando mixup. O método cria uma nova instância de Point com o valor x de self Point (do tipo T) e o valor y do passado Point (do tipo W).

Nome do arquivo: src/main.rs

struct Point<T, U> {
    x: T,
    y: U,
}

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };

    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

Listagem 10-11: Um método que usa diferentes tipos genéricos da definição de sua estrutura

Em main, definimos um Point que possui um i32 para x (com valor 5) e um f64 para y (com valor 10.4). A variável p2 é uma estrutura Point que possui uma fatia de string para x (com valor "Hello") e uma char para y (com valor c). Chamando mixup em p1 com o argumento p2 nos dá p3, que terá um i32 para x, porque x veio de p1. A variável p3 terá um char para y, porque y veio de p2. A chamada da macro println! será impressa p3.x = 5, p3.y = c.

O objetivo deste exemplo é demonstrar uma situação em que alguns parâmetros genéricos são declarados com impl e alguns são declarados com a definição do método. Aqui, os parâmetros genéricos T e U são declarados depois de impl, porque vão com a definição da estrutura. Os parâmetros genéricos V e W são declarados depois de fn mixup, porque são relevantes apenas para o método.

Desempenho do código usando genéricos

Você pode estar se perguntando se há um custo de tempo de execução ao usar parâmetros de tipo genérico. A boa notícia é que o Rust implementa genéricos de forma que seu código não execute mais devagar usando tipos genéricos do que faria com tipos concretos.

Rust faz isso realizando a monomorfização do código que está usando genéricos em tempo de compilação. Monomorfização é o processo de transformar código genérico em código específico, preenchendo os tipos concretos que são usados quando compilados.

Nesse processo, o compilador faz o oposto das etapas que usamos para criar a função genérica na Listagem 10-5: o compilador examina todos os lugares onde o código genérico é chamado e gera o código para os tipos concretos com os quais o código genérico é chamado.

Vejamos como isso funciona com um exemplo que usa o enum Option<T> da biblioteca padrão:


#![allow(unused)]
fn main() {
let integer = Some(5);
let float = Some(5.0);
}

Quando o Rust compila esse código, ele realiza a monomorfização. Durante esse processo, o compilador lê os valores que foram usados ​​nas instâncias de Option<T> e identifica dois tipos de Option<T>: um é i32 e o outro é f64. Como tal, ele expande a definição genérica de Option<T> em Option_i32 e Option_f64, assim, substituindo a definição genérica por outras específicas.

A versão monomorfizada do código se parece com o seguinte. O genérico Option<T> é substituído pelas definições específicas criadas pelo compilador:

Nome do arquivo: src/main.rs

enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}

Como o Rust compila o código genérico em um código que especifica o tipo em cada instância, não pagamos nenhum custo de tempo de execução pelo uso de genéricos. Quando o código é executado, ele funciona exatamente como se tivéssemos duplicado cada definição manualmente. O processo de monomorfização torna os genéricos de Rust extremamente eficientes em tempo de execução.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

quinta-feira, 15 de abril de 2021

Tipos, características e tempos de vida genéricos em Rust

Cada linguagem de programação possui ferramentas para lidar de forma eficaz com a duplicação de conceitos. No Rust, uma dessas ferramentas são os genéricos. Os genéricos são substitutos abstratos para tipos concretos ou outras propriedades. Quando estamos escrevendo código, podemos expressar o comportamento dos genéricos ou como eles se relacionam com outros genéricos sem saber o que estará em seu lugar ao compilar e executar o código.

Semelhante à maneira como uma função usa parâmetros com valores desconhecidos para executar o mesmo código em vários valores concretos, as funções podem usar parâmetros de algum tipo genérico em vez de um tipo concreto, como i32 ou String. Na verdade, já usamos os genéricos no Capítulo 6 com Option<T>, no Capítulo 8 com Vec<T> e HashMap<K, V> e no Capítulo 9 com Result<T, E>. Neste capítulo, você explorará como definir seus próprios tipos, funções e métodos com genéricos!

Primeiro, vamos revisar como extrair uma função para reduzir a duplicação de código. A seguir, usaremos a mesma técnica para fazer uma função genérica a partir de duas funções que diferem apenas nos tipos de seus parâmetros. Também explicaremos como usar tipos genéricos em definições de struct e enum.

Em seguida, você aprenderá como usar características para definir o comportamento de uma forma genérica. Você pode combinar características com tipos genéricos para restringir um tipo genérico apenas aos tipos que têm um comportamento específico, em oposição a qualquer tipo.

Finalmente, discutiremos os tempos de vida, uma variedade de genéricos que fornecem ao compilador informações sobre como as referências se relacionam entre si. Os tempos de vida nos permitem pegar valores emprestados em muitas situações, enquanto ainda permitem que o compilador verifique se as referências são válidas.

Removendo Duplicação Extraindo uma Função

Antes de mergulhar na sintaxe dos genéricos, vamos primeiro ver como remover a duplicação que não envolve tipos genéricos extraindo uma função. Em seguida, aplicaremos essa técnica para extrair uma função genérica! Da mesma forma que você reconhece o código duplicado para extrair em uma função, você começará a reconhecer o código duplicado que pode usar genéricos.

Considere um programa curto que encontra o maior número em uma lista, conforme mostrado na Listagem 10-1.

Nome do arquivo: src/main.rs

fn main() {
    let lista_de_numeros = vec![34, 50, 25, 100, 65];

    let mut maior = lista_de_numeros[0];

    for numero in lista_de_numeros {
        if numero > maior {
            maior = numero;
        }
    }

    println!("O maior número é {}", maior);
    assert_eq!(maior, 100);
}

Listagem 10-1: Código para encontrar o maior número em uma lista de números

Este código armazena uma lista de inteiros na variável lista_de_numeros e coloca o primeiro número da lista em uma variável chamada maior. Em seguida, ele itera por todos os números da lista e, se o número atual for maior do que o número armazenado em maior, ele substitui o número dessa variável. No entanto, se o número atual for menor ou igual ao maior número visto até agora, a variável não muda e o código passa para o próximo número na lista. Depois de considerar todos os números da lista, maior deve conter o maior número, que neste caso é 100.

Para encontrar o maior número em duas listas diferentes de números, podemos duplicar o código da Listagem 10-1 e usar a mesma lógica em dois locais diferentes no programa, conforme mostrado na Listagem 10-2.

Nome do arquivo: src/main.rs

fn main() {
    let lista_de_numeros = vec![34, 50, 25, 100, 65];

    let mut maior = lista_de_numeros[0];

    for numero in lista_de_numeros {
        if numero > maior {
            maior = numero;
        }
    }

    println!("O maior número é {}", maior);

    let lista_de_numeros = vec![102, 34, 6000, 89, 54, 2, 43, 8];

    let mut maior = lista_de_numeros[0];

    for numero in lista_de_numeros {
        if numero > maior {
            maior = numero;
        }
    }

    println!("O maior número é {}", maior);
}

Listagem 10-2: Código para encontrar o maior número em duas listas de números

Embora esse código funcione, duplicá-lo é tedioso e sujeito a erros. Também temos que atualizar o código em vários lugares quando quisermos alterá-lo.

Para eliminar essa duplicação, podemos criar uma abstração definindo uma função que opera em qualquer lista de inteiros fornecidos a ela em um parâmetro. Essa solução torna nosso código mais claro e nos permite expressar o conceito de encontrar o maior número em uma lista de maneira abstrata.

Na Listagem 10-3, extraímos o código que encontra o maior número em uma função chamada maior. Ao contrário do código da Listagem 10-1, que pode localizar o maior número em apenas uma lista específica, este programa pode localizar o maior número em duas listas diferentes.

Nome do arquivo: src/main.rs

fn maior(list: &[i32]) -> &i32 {
    let mut maior = &list[0];

    for item in list {
        if item > maior {
            maior = item;
        }
    }

    maior
}

fn main() {
    let lista_de_numeros = vec![34, 50, 25, 100, 65];

    let result = maior(&lista_de_numeros);
    println!("O maior número é {}", result);
    assert_eq!(result, &100);

    let lista_de_numeros = vec![102, 34, 6000, 89, 54, 2, 43, 8];

    let result = maior(&lista_de_numeros);
    println!("O maior número é {}", result);
    assert_eq!(result, &6000);
}

Listagem 10-3: código abstraído para encontrar o maior número em duas listas

A função maior tem um parâmetro chamado list, que representa qualquer fatia concreta de valores i32 que podemos passar para a função. Como resultado, quando chamamos a função, o código é executado nos valores específicos que passamos.

Em suma, aqui estão as etapas que seguimos para alterar o código da Listagem 10-2 para a Listagem 10-3:

  1. Identifique o código duplicado.
  2. Extraia o código duplicado no corpo da função e especifique as entradas e os valores de retorno desse códigona assinatura da função.
  3. Atualize as duas instâncias de código duplicado para chamar a função.

A seguir, usaremos essas mesmas etapas com os genéricos para reduzir a duplicação de código de maneiras diferentes. Da mesma forma que o corpo da função pode operar em um abstrato em list em vez de valores específicos, os genéricos permitem que o código opere em tipos abstratos.

Por exemplo, digamos que temos duas funções: uma que encontra o maior item em uma fatia de valores i32 e outra que encontra o maior item em uma fatia de valores char. Como eliminaríamos essa duplicação? Vamos descobrir!

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença