Traits

Na parte sobre generics deixamos o seguinte código por terminar.

fn maior<T>(lista: &[T]) -> T {
    let mut maior = lista[0];
    for &item in lista {
        if item > maior {
            maior = item;
        }
    }
    maior
}

fn main() {
    let arr: [u8; 4] = [2,4,1,11];
    let maior = maior(&arr);
    println!("Maior elemento: {}", maior);
}

Afinal o que falta para esse código funciona? Falta determinamos que 'T' deve implementar algumas traits. O que são essas traits?? Elas são como contratos, um tipo deve implementar certas funções/métodos definidas por essa trait. E como declaramos uma? Seguimos o seguinte padrão trait nome { assinaturas/métodos }.


#![allow(unused)]
fn main() {
trait Pagavel  {
    fn total(&self) -> f64;
}
}

Assim definimos uma trait, agora precisamos implementar, vamos criar uma struct Pedido que ira implementar essa trait. Para dizer que algo implementa uma trait usamos o seguinte padrão impl NomeTrait for NomeStruct { implementação }

trait Pagavel  {
    fn total(&self) -> f64;
}
//--definição trait Pagável

struct Pedido {
    quantidade_items: u8,
    valor_items: f64
}

impl Pagavel for Pedido {
    fn total(&self) -> f64 {
        self.valor_items * self.quantidade_items as f64
    }
}

fn main() {
    let pedido = Pedido {
        quantidade_items: 10,
        valor_items: 10.5
    };
    let total = pedido.total();
    println!("Total do pedido: {}", total);
}

Agora podemos falar que nossa struct de Pedido implementa esta Trait. E agora se vamos utilizar um método genérico que tenha 'T' como parâmetro, porém queremos que 'T' seja pagável. Como faríamos isso? Temos dois modos, sendo eles:


#![allow(unused)]
fn main() {
fn pagar<T: Pagavel>(pagavel: T) {
    println!("Valor {} pago", pagavel.total());
}
}

Ou com a palavra 'where'


#![allow(unused)]
fn main() {
fn pagar<T>(pagavel: T)
where
    T: Pagavel,
{
    println!("Valor {} pago", pagavel.total());
}
}

Então podemos chamar o método genérico pagar passando o pedido como argumento.

trait Pagavel  {
    fn total(&self) -> f64;
}
struct Pedido {
    quantidade_items: u8,
    valor_items: f64
}
impl Pagavel for Pedido {
    fn total(&self) -> f64 {
        self.valor_items * self.quantidade_items as f64
    }
}
fn pagar<T>(pagavel: T)
where
    T: Pagavel,
{
    println!("Valor {} pago", pagavel.total());
}
//--Definição pedido, trait e implementação
fn main() {
    let pedido = Pedido {
        quantidade_items: 10,
        valor_items: 10.5,
    };
    pagar(pedido);
}

E o programa ira compilar.

Podemos implementar mais de uma trait para algo.

trait Pagavel  {
    fn total(&self) -> f64;
}
struct Pedido {
    quantidade_items: u8,
    valor_items: f64
}
impl Pagavel for Pedido {
    fn total(&self) -> f64 {
        self.valor_items * self.quantidade_items as f64
    }
}
//--declaração trait pagavel, struct e impl Pagavel
trait Cancelavel {
    fn cancelar(self);
}

impl Cancelavel for Pedido {
    fn cancelar(self) {
        println!("Pedido com {} itens cancelado", self.quantidade_items)
    }
}

fn main() {
    let pedido = Pedido {
        quantidade_items: 10,
        valor_items: 10.5,
    };
    pedido.cancelar();
}

Uma trait pode ter um método já implementado, que pode ou não ser sobrescrito.

trait Pagavel  {
    fn total(&self) -> f64;
}
struct Pedido {
    quantidade_items: u8,
    valor_items: f64
}
impl Pagavel for Pedido {
    fn total(&self) -> f64 {
        self.valor_items * self.quantidade_items as f64
    }
}
trait Cancelavel {
   fn cancelar(self);
}
impl Cancelavel for Pedido {
   fn cancelar(self) {
       println!("Pedido com {} itens cancelado", self.quantidade_items)
   }
}
//--declaração trait pagavel, struct e impl Pagavel e cancelavel

trait Tributavel {
    fn calcular_imposto(&self) -> f64 {
        0.01 * 200.0
    }
}

impl Tributavel for Pedido {}

fn main() {
    let pedido = Pedido {
        quantidade_items: 10,
        valor_items: 10.5,
    };
    pedido.cancelar();
}

Caso queira sobrescrever a implementação de Tributavel seria feito como a implementação de qualquer outra trait.


#![allow(unused)]
fn main() {
impl Tributavel for Pedido {
    fn calcular_imposto(&self) -> f64 {
        self.valor_items * 0.01
    }
}
}

E se eu quiser que para implementar Tributavel e Cancelavel eu precise implementar a trait Pagavel? Seria usado uma estratégia parecida com a dos generics trait NomeTrait: TraitQuePrecisaImplementar


#![allow(unused)]
fn main() {
trait Pagavel {
    fn total(&self) -> f64;
}

trait Cancelavel: Pagavel {
    fn cancelar(self);
}

trait Tributavel: Pagavel {
    fn calcular_imposto(&self) -> f64 {
        0.01 * 200.0
    }
}
}

Do modo acima para implementar Cancelavel ou Tributavel precisamos implementar Pagavel, assim nos dando um novo poder nas implementações, PODER USAR OS MÉTODOS DEFINIDOS EM PAGAVEL.


#![allow(unused)]
fn main() {
impl Cancelavel for Pedido {
    fn cancelar(self) {
        println!("Pedido custando {} cancelado", self.total())
    }
}

impl Tributavel for Pedido {
    fn calcular_imposto(&self) -> f64 {
        0.01 * self.total()
    }
}
}

E se eu implementar duas traits com métodos iguais? Não podemos chamar diretamente o método implementado.

struct Cachorro {}

trait Animal {
    fn comer(&self);
}

trait Fome {
    fn comer(&self);
}

impl Animal for Cachorro {
    fn comer(&self) {
        println!("Cachorro comendo... animal");
    }
}

impl Fome for Cachorro {
    fn comer(&self) {
        println!("Cachorro comendo por estar com fome");
    }
}

fn main() {
    let cachorro = Cachorro {};
    cachorro.comer();
}

Perdão pelo exemplo bobo, mas o código acima daria o seguinte erro.

error[E0034]: multiple applicable items in scope
  --> src/main.rs:25:14
   |
25 |     cachorro.comer();
   |              ^^^^^ multiple `comer` found
   |

Este erro acontece por termos múltiplas implementações de método com a mesma assinatura. Para chamar o método comer podemos fazer da seguinte maneira trait::metodo(&instância).

struct Cachorro {}
trait Animal {
    fn comer(&self);
}
trait Fome {
    fn comer(&self);
}
impl Animal for Cachorro {
    fn comer(&self) {
        println!("Cachorro comendo... animal");
    }
}
impl Fome for Cachorro {
    fn comer(&self) {
        println!("Cachorro comendo por estar com fome");
    }
}
//--declarações e implementações
fn main() {
    let cachorro = Cachorro {};
    Animal::comer(&cachorro);
    Fome::comer(&cachorro);
}

Podemos também implementar traits para tipos já existentes.

trait Fome {
    fn comer(&self);
}
//--declaração trait de fome
impl Fome for i32 {
    fn comer(&self) {
        println!("Um numero esta comendo por estar com fome? Isso faz sentido?")
    }
}

fn main() {
    let a: i32 = 10;
    a.comer();
}

Claro essa implementação de uma trait teria que fazer sentido, não é mesmo?

Traits já existentes

Em Rust já temos uma boa quantidade de traits já existentes, como, por exemplo, a trait Iterator, com essa trait podemos criar nossas próprias implementações de algo iterável e utilizar os recursos da linguagem, como um loop for, por exemplo.

struct Contador {
    contagem: u64
}

impl Iterator for Contador {
    type Item = u64; /*futuramente iremos explicar com mais detalhes o que é isso,
    mas considere que é um modo de usar Generics de uma forma que impedimos múltiplas implementações da mesma trait pra mesma coisa */

    fn next(&mut self) -> Option<Self::Item> {
        if self.contagem >= 100 {
            None
        } else {
            self.contagem += 1;
            Some(self.contagem)
        }
    }
}

fn main() {
    let contador = Contador { contagem: 0 };

    for i in contador {
        println!("Numero atual: {}", i);
    }
}

Ao executar o código acima teremos a saída.

...
Numero atual: 89
Numero atual: 90
Numero atual: 91
Numero atual: 92
Numero atual: 93
Numero atual: 94
Numero atual: 95
Numero atual: 96
Numero atual: 97
Numero atual: 98
Numero atual: 99
Numero atual: 100

Derive

O comando #[derive(AlgumaCoisaAqui)], é um macro para implementação de algumas traits, quando usamos o #[Derive(Debug)] estamos informando ao compilador que queremos que aquela struct/enum ira implementar a trait Debug, porém isso é gerado de forma automática pelo compilador.

Voltando ao problema inicial.

Agora que entendemos como as traits funcionam vamos retomar o problema que deixamos no fim da parte anterior

Precisamos limitar 'T' para duas traits especificas, essas traits já são existentes na linguagem, sendo elas PartialOrd e Copy, para falar que o argumento precisa implementar mais de uma trait utilizamos o '+', com o seguinte padrão 'T: Trait1 + Trait2 + Trait3....`

fn maior<T>(lista: &[T]) -> T
where
    T: PartialOrd + Copy,
{
    let mut maior = lista[0];
    for &item in lista {
        if item > maior {
            maior = item;
        }
    }
    maior
}

fn main() {
    let arr: [u8; 4] = [2, 4, 1, 11];
    let maior = maior(&arr);
    println!("Maior elemento: {}", maior);
}

Ao executar o nosso código finalmente terá sucesso e a seguinte saída no console.

Maior elemento: 11

Este capítulo sobre traits ficou maior que do eu esperava, mas espero que tenha ficado claro o uso delas e a importância dessa funcionalidade.