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

Um comentário: