Traits: Definindo Comportamento Compartilhado

Traits nos permitem usar outro tipo de abstração: eles nos permitem abstrair sobre o comportamento que tipos têm em comum. Um trait diz ao compilador de Rust sobre uma funcionalidade que um tipo particular possui e pode compartilhar com outros tipos. Em situações onde nós usamos parâmetros de tipos genéricos, nós podemos usar limites de trait para especificar, em tempo de compilação, que o tipo genérico pode ser qualquer tipo que implementa um trait e por conseguinte tem o comportamento que queremos usar nessa situação.

Nota: Traits são similares a um recurso frequentemente chamado de 'interface' em outras linguagens, com algumas diferenças.

Definindo um Trait

O comportamento de um tipo consiste nos métodos que podemos chamar para aquele tipo. Tipos diferentes dividem o mesmo comportamento se podemos chamar os mesmos métodos em todos esses tipos. Definições de traits são um modo de agrupar métodos de assinaturas juntos a fim de definir um conjunto de comportamentos para atingir algum propósito.

Por exemplo, digamos que temos múltiplos structs que contém vários tipos e quantidades de texto: um struct ArtigoDeNoticiasque contém uma notícia preenchida em um lugar do mundo, e um Tweet que pode ter no máximo 140 caracteres em seu conteúdo além dos metadados como se ele foi um retweet ou uma resposta a outro tweet.

Nós queremos fazer uma biblioteca agregadora de mídia que pode mostrar resumos de dados que podem estar guardados em uma instância de ArtigoDeNoticia ou Tweet. O comportamento que precisamos cada struct possua é que seja capaz de ser resumido, e que nós possamos pedir pelo resumo chamando um método resumo em uma instância. A Listagem 10-12 mostra a definição de um trait Resumir que expressa esse conceito:

Nome do arquivo: lib.rs


# #![allow(unused_variables)]
#fn main() {
pub trait Resumir {
    fn resumo(&self) -> String;
}
#}

Listagem 10-12: Definição de um trait Resumir que consiste no comportamento fornecido pelo método resumo

Nós declaramos um trait com a palavra-chave trait, e então o nome do trait, nesse caso Resumir. Dentro de chaves declaramos a assinatura do método que descreve o comportamento que tipos que implementam esse trait precisarão ter, nesse caso fn resumo(&self) -> String;. Depois da assinatura do método ao invés de fornecer uma implementação dentro de chaves, nós colocamos um ponto e vírgula. Cada tipo que implementa esse trait precisa então fornecer seu próprio comportamento customizado para o corpo do método, mas o compilador vai reforçar que qualquer qualquer tipo que tenha o trait Resumir terá o método resumo definido para ele com esse exata assinatura.

Um trait pode ter vários métodos no seu corpo, com os métodos das assinaturas listados um por linha e cada linha terminando com um ponto e vírgula.

Implementando um Trait em um Tipo

Agora que deifnimos o trait Resumir, podemos implementa-lo nos tipos do nosso agregador de mídias que queremos que tenham esse comportamento. A Listagem 10-13 mostra uma implementação do trait Resumir no struct ArtigoNotícia que possui o título, o autor e a localização para criar e retornar o valor de resumo. Para o struct Tweet, nós escolhemos definir resumo como o nome de usuário seguido por todo o texto do tweet, assumindo que o conteúdo do tweet já está limitado a 140 caracteres.

Nome do arquivo: lib.rs


# #![allow(unused_variables)]
#fn main() {
# pub trait Resumir {
#     fn summary(&self) -> String;
# }
#
pub struct ArtigoDeNoticia {
    pub titulo: String,
    pub local: String,
    pub autor: String,
    pub conteudo: String,
}

impl Resumir for ArtigoDeNoticia {
    fn summary(&self) -> String {
        format!("{}, by {} ({})", self.titulo, self.autor, self.local)
    }
}

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

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

Listagem 10-13: Implementando o trait Resumir nos tipos ArtigoDeNoticia e Tweet

Implementar um trait em um tipo é similar a implementar métodos que não estão relacionados com um trait. A diferença está depois de impl, nós colocamos o nome do trait que queremos implementar, então dizemos for e o nome do tipo que queremos implementar. Dentro do bloco impl, nós colocamos as assinaturas dos métodos que a definição do trait definiu, mas ao invés de colocar um ponto e vírgula depois de cada assinatura, nós colocamos chaves e preenchemos o corpo do método com o comportamento específico que queremos que os métodos dos traits tenham para um tipo particular.

Uma vez que implementamos o trait, nós podemos chamar os métodos nas instâncias de ArtigoDeNoticia e Tweet da mesma maneira que nós chamamos métodos que não são parte de um trait:

let tweet = Tweet {
    nomeUsuario: String::from("horse_ebooks"),
    conteudo: String::from("claro, como vocês provavelmente já sabem, 
    pessoas"),
    resposta: false,
    retweet: false,
};

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

Isso irá imprimir 1 novo tweet: claro, como vocês provavelmente já sabem, pessoas

Note que porque nós definimos o trait Resumir e os tipos ArtigoDeNoticia e Tweet todos na mesma lib.rs na listagem 10-13, eles estão todos no mesmo escopo. Se essa lib.rs é para um crate nós chamamos agregador, e se outra pessoa quiser usar a funcionalidade do nosso crate e implementar o trait Resumir na sua struct PrevisaoTempo, o código deles precisaria importar o trait Resumir no escopo deles primeiro antes deles poderem implementá-lo, como na Listagem 10-14:

Nome do arquivo: lib.rs

extern crate aggregator;

use aggregator::Resumir;

struct PrevisaoTempo {
    alta_temp: f64,
    baixa_temp: f64,
    chance_de_chuva: f64,
}

impl Resumir for PrevisaoTempo {
    fn resumo(&self) -> String {
        format!("A alta será de {}, e a baixa de {}. A chance de precipitação é
        {}%.", self.alta_temp, self.baixa_temp, self.chance_de_chuva)
    }
}

Listagem 10-14: Trazendo o trait Resumir do nosso crate aggregator para o escopo de outro crate

Esse código também assume que Resumir é um trait público, o que é verdade porque colocamos a palavra-chave pub antes de trait na Listagem 10-12.

Uma restrição para se prestar atenção na implementação de traits: nós podemos implementar um trait em um tipo desde que o trait ou o tipo forem locais para o nosso crate. Em outras palavras, nós não estamos autorizados a implementar traits externos em tipos externos. Nós não podemos implementar o trait Display em Vec, por exemplo, já que ambos Display e Vec são definidos na biblioteca padrão. Nós temos a permissão de implementar traits da biblioteca padrão como Display em um tipo personalizado como Tweet como parte da funcionalidade do nosso crate aggregator, já que nós já havíamos definido Resumir lá. Essa restrição é parte do que é chamado de a regra do ṍrfão, qual você pode procurar se estiver interessado nesse tipo de teoria. De forma curta, é chamada de a regra do órfão porque o tipo pai não está presente. Sem essa regra, dois crates poderiam implementar o mesmo trait para o mesmo tipo, e as duas implementações entrariam em conflito: o Rust não saberia qual implementação usar. Porque o Rust impõe a regra do órfão, os códigos de outras pessoas não podem quebrar seu código e vice e versa.

Implementações Padrão

As vezes é útil ter um comportamento padrão pra alguns ou todos os métodos em um trait, ao invés de fazer toda implementação em todo tipo e definir um comportamento personalizado. Quando implementamos o trait em um tipo particular, nós podemos escolher manter ou sobrescrever o comportamento padrão de cada método.

A Listagem 10-15 mostra como poderíamos ter escolhido especificar uma string padrão para o método resumo do trait Resumir ao invés de escolher de apenas definir a assinatura do método como fizemos na Listagem 10-12:

Nome do arquivo: lib.rs


# #![allow(unused_variables)]
#fn main() {
pub trait Resumir {
    fn resumo(&self) -> String {
        String::from("(Leia mais...)")
    }
}
#}

Listagem 10-15: Definição de um trait Resumir com a implementação padrão do método resumo

Se nós quiséssemos usar a implementação padrão para resumir as instâncias de ArtigoDeNoticia ao invés de definir uma implementação personalizada como fizemos na Listagem 10-13, nós especificaríamos um bloco impl vazio:

impl Resumir for ArtigoDeNoticia {}

Mesmo que não estejamos mais escolhendo definir o método resumo diretamente em ArtigoDeNoticia, já que o método resumo tem uma implementação padrão e nós especificamos que ArtigoDeNoticia implementa o trait Resumir, nós ainda podemos chamar o método resumo em uma instância de ArtigoDeNoticia:

let artigo = ArtigoDeNoticia {
    titulo: String::from("Os Penguins ganham a copa do campeonato Stanley"),
    lugar: String::from("Pittsburgh, PA, USA"),
    autor: String::from("Iceburgh"),
    conteudo: String::from("Os Penguins de Pittsburgh são novamente o melhor
    time de hockey da NHL."),
};

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

Esse código imprime Novo artigo disponível! (Leia mais...)

Mudando o trait Resumir para ter uma implementação padrão para resumo não requer que nós mudemos nada na implementação de Resumir em Tweet na Listagem 10-13 ou em PrevisaoTempo na Listagem 10-14: a sintaxe para sobrepor uma implementação padrão é exatamente a mesma de uma sintaxe para implementar um método de trait que não tem uma implementação padrão.

Implementações padrões são autorizadas a chamar outros métodos no mesmo trait, mesmo se os outros métodos não tiverem uma implementação padrão. Desse modo, um trait pode prover muitas funcionalidades úteis e apenas requerir implementações para especificar uma pequena parte dele. Nós poderíamos escolher que o trait Resumir também tivesse o método resumo_autor qual a implementação é necessária, então um método resumo que tem a implementação padrão que chama pelo método resumo_autor:


# #![allow(unused_variables)]
#fn main() {
pub trait Resumir {
    fn resumo_autor(&self) -> String;

    fn resumo(&self) -> String {
        format!("(Leia mais de {}...)", self.resumo_autor())
    }
}
#}

Para usar essa versão de Resumir, nós só precisamos definir resumo_autor quando nós implementamos o trait em um tipo:

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

Uma vez que definimos resumo_autor, nós podemos chamar resumo em instâncias do struct Tweet, e a implementação padrão de resumo chamará a definição de resumo_autor que fornecemos.

let tweet = Tweet {
    nomeusuario: String::from("horse_ebooks"),
    conteudo: String::from("claro, como vocês provavelmente já sabem, 
    pessoas"),
    resposta: false,
    retweet: false,
};

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

Isso irá imprimir 1 novo tweet: (Leia mais de @horse_ebooks...).

Note que não é possível chamar a implementação padrão de uma implementação primordial.

Limites de traits

Agora que definimos traits e os implementamos em tipos, podemos usar traits com parâmetros de tipos genéricos. Podemos restringir tipos genéricos para que ao invés de serem qualquer tipo, o compilador tenha certeza que o tipo estará limitado a aqueles tipos que implementam um trait em particular e por consequência tenham o comportamento que precisamos que os tipos tenham. Isso é chamado de especificar os limites dos traits em um tipo genérico.

Por exemplo, na Listagem 10-13, nós implementamos o trait Resumir nos tipos ArtigoDeNoticia e Tweet. Nós podemos definir uma função notificar que chama o método resumo no seu parâmetro item, que é do tipo genérico T. Para ser possível chamar resumo em item sem receber um erro, podemos usar os limites de traits em T para especificar que item precisa ser de um tipo que implementa o trait Resumir:

pub fn notificar<T: Resumir>(item: T) {
    println!("Notícias de última hora! {}", item.resumo());
}

Limites de traits vão juntos com a declaração de um parâmetro de tipo genérico, depois de uma vírgula e entre colchetes angulares. Por causa do limite de trait em T, nós podemos chamar notificar e passar qualquer instância de ArtigoDeNoticia ou Tweet. O código externo da Listagem 10-14 que está usando nosso crate aggregator pode chamar nossa função notificar e passar uma instância de PrevisaoTempo, já que Resumir é implementado para PrevisaoTempo também. O código que chama notificar com qualquer outro tipo, como uma String ou um i32, não compilará, já que esses tipos não implementam Resumir.

Nós podemos especificar múltiplos limites de traits em um tipo genérico usando +. Se nós precisássemos ser capazes de usar mostrar formatação no tipo T em uma função assim como no método resumo, nós podemos usar os limites de trait T: Resumir + Mostrar. Isso signifca que T pode ser qualquer tipo que implemente ambos Resumir e Mostrar.

Para funções que têm múltiplos parâmetros de tipos genéricos, cada tipo genérico tem seu próprio limite de trait. Especificar muitas informações de limites de trait dentro de chaves angulares entre o nome de uma função e sua lista de parâmetros pode tornar o código difícil de ler, então há uma sintaxe alternativa para especificar limites de traits que nos permite movê-los para uma cláusula depois da assinatura da função. Então ao invés de:

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

Nós podemos escrever isso com uma cláusula de where:

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

Isso é menos confuso e faz a assinatura da função ficar mais parecida à uma função sem ter vários limites de trait, nela o nome da função, a lista de parâmetros, e o tipo de retorno estão mais próximos.

Consertando a Função maior com Limites de Traits

Então qualquer hora que você queira usar um comportamento definido por um trait em um tipo genérico, você precisa especificar aquele trait nos limites dos parâmetros dos tipos genéricos. Agora podemos consertar a definição da função maior que usa um parâmetro de tipo genérico da Listagem 10-5! Quando deixamos esse código de lado, nós recebemos esse erro:

error[E0369]: binary operation `>` cannot be applied to type `T`
  |
5 |         if item > maior {
  |            ^^^^
  |
note: an implementation of `std::cmp::PartialOrd` might be missing for `T`

No corpo de maior nós queríamos ser capazes de comparar dois valores de tipo T usando o operador maior-que. Esse operador é definido com o método padrão na biblioteca padrão de trait std::cmp::PartialOrd. Então para que possamos usar o operador maior-que, precisamos especificar PartialOrd nos limites do trait para T para que a função maior funcione em partes de qualquer tipo que possa ser comparada. Não precisamos trazer PartialOrd para o escopo porque está no prelúdio.

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

Se tentarmos compilar isso, receberemos diferentes erros:

error[E0508]: cannot move out of type `[T]`, a non-copy array
 --> src/main.rs:4:23
  |
4 |     let mut maior = list[0];
  |         -----------   ^^^^^^^ cannot move out of here
  |         |
  |         hint: to prevent move, use `ref maior` or `ref mut maior`

error[E0507]: cannot move out of borrowed content
 --> src/main.rs:6:9
  |
6 |     for &item in list.iter() {
  |         ^----
  |         ||
  |         |hint: to prevent move, use `ref item` or `ref mut item`
  |         cannot move out of borrowed content

A chave para esse erro é cannot move out of type [T], a non-copy array. Com nossas versões não genéricas da função maior, nós estávamos apenas tentando encontrar o maior i32 ou char. Como discutimos no Capítulo 4, tipos como o i32 e char que têm um tamanho conhecido podem ser armazenados na pilha, então eles implementam o trait Copia. Quando mudamos a função maior para ser genérica, agora é possível que o parâmetro list poderia ter tipos nele que não implementam o trait Copia, o que significa que não seríamos capazes de mover o valor para fora de list[0] para a variável maior.

Se quisermos ser capazes de chamar esse código com tipos que são Copia, nós podemos adicionar Copia para os limites de trait de T! A Listagem 10-16 mostra o código completo de uma função maior genérica que compilará desde que os tipos dos valores nessa parte que passamos para maior implementem ambos os traits PartialOrd e Copia, 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.iter() {
        if item > maior {
            maior = item;
        }
    }

    maior
}

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

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

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

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

Listagem 10-16: Uma definição funcional da função maior que funciona em qualquer tipo genérico que implementa os traits PartialOrd e Copia

Se não quisermos restringir nossa função maior para apenas tipos que implementam o trait Copia, podemos especificar que T tem o limite de trait Clone ao invés de Copia e clonar cada valor na parte quando quisermos que a função maior tenha domínio. Usando a função clone significa que potencialmente estamos fazendo mais alocações no heap, porém, e alocações no heap podem ser vagarosas se estivermos trabalhando com grande quantidade de dados. Outro jeito que podemos implementar maior é para a função retornar uma referência ao valor de T em uma parte. Se retornarmos o tipo de retorno para ser &T ao invés de T e mudar o corpo da função para retornar uma referência, não precisaríamos usar os limites de traits Clone ou Copia e nós não estaríamos fazendo nenhuma alocação de heap. Tente implementar essas soluções alternativas você mesmo!

Usando Limites de Trait para Implementar Métodos Condicionalmente

Usando um limite de trait com um bloco impl que usa parâmetros de tipos genéricos podemos implementar métodos condicionalmente apenas para tipos que implementam os traits específicos. Por exemplo, o tipo Par<T> na listagem 10-17 sempre implementa o método novo, mas Par<T> implementa apenas o cmp_display se seu tipo interno T implementa o trait PartialOrd que permite a comparação e do trait Display que permite a impressão:


# #![allow(unused_variables)]
#fn main() {
use std::fmt::Display;

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

impl<T> Par<T> {
    fn novo(x: T, y: T) -> Self {
        Self {
            x,
            y,
        }
    }
}

impl<T: Display + PartialOrd> Par<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-17: Implementa métodos condicionalmente em um tipo genérico dependendo dos limites de trait

Podemos também condicionalmente implementar um trait para qualquer tipo que implementa um trait. Implementações de trait de qualquer tipo que satisfazem os limites de trait são chamadas de implementações cobertores, e são extesivamente utilizadas na biblioteca padrão de Rust. Por exemplo, a biblioteca padrão implementa o trait Display. Esse bloco impl se parece com este código:

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

Porque a biblioteca padrão tem essa implementação cobertor, podemos chamar o método to_string definido pelo tipo ToString em qualquer tipo que implemente o trait Display. Por exemplo, nós podemos transformar inteiros em seus correspondentes valores de String do seguinte modo, já que inteiros implementam Display:


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

Implementações cobertor aparecem na documentação para traits na seção "Implementadores".

Traits e limites de traits nos deixam escrever código que usam parâmetros de tipos genéricos para reduzir a duplicação, mas ainda sim especificam para o compilador exatamente qual o comportamento que nosso código precisa que o tipo genérico tenha. Porque demos a informação do limite de trait para o compilador, ele pode checar que todos os tipos concretos usados no nosso código proporcionam o comportamento correto. Em linguagens dinamicamente tipadas, se nós tentássemos chamar um método em um tipo que não implementamos, nós receberíamos um erro em tempo de execução. O Rust move esses erros para o temp de compilação para que possamos ser forçados a resolver os problemas antes que nosso código seja capaz de rodar. Além disso, nós não temos que escrever código que checa o comportamento em tempo de execução já que já checamos em tempo de compilação, o que melhora o desempenho comparado com outras linguagens sem ter que abrir mão da flexibilidade de tipos genéricos.

Há outro tipo de tipos genéricos que estamos usando sem nem ao menos perceber chamados lifetimes. Em vez de nos ajudar a garantir que um tipo tenha o comportamento que precisamos, lifetimes nos ajudam a garantir que as referências são válidas tanto quanto precisam ser. Vamos aprender como lifetimes fazem isso.