Definir o comportamento compartilhado usando características

Concluído

Uma característica é uma interface comum que um grupo de tipos pode implementar. A biblioteca padrão do Rust tem muitas características úteis, como:

  • io::Read para valores que podem ler bytes de uma origem.
  • io::Write para valores que podem gravar bytes.
  • Debug para valores que podem ser impressos no console usando o especificador de formato "{:?}".
  • Clone para valores que podem ser duplicados explicitamente na memória.
  • ToString para valores que podem ser convertidos em um String.
  • Default para tipos que têm um valor padrão sensato, como zero para números, vazio para vetores e "" para String.
  • Iterator para tipos que podem produzir uma sequência de valores.

Cada definição de característica é uma coleção de métodos definidos para um tipo desconhecido, geralmente representando uma funcionalidade ou comportamento que seus implementadores podem apresentar.

Para representar o conceito de "ter uma área bidimensional", podemos definir a seguinte característica:

trait Area {
    fn area(&self) -> f64;
}

Aqui, declaramos uma característica usando a palavra-chave trait e o nome da característica, que é Area neste caso.

Dentro das chaves, declaramos as assinaturas do método que descrevem os comportamentos dos tipos que implementam essa característica, que neste caso é a assinatura de função fn area(&self) -> f64. O compilador, então, verificará se cada tipo que implementa essa característica deve fornecer um comportamento personalizado para o corpo do método.

Agora, vamos criar alguns tipos que implementarão nossa característica Area:

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Area for Circle {
    fn area(&self) -> f64 {
        use std::f64::consts::PI;
        PI * self.radius.powf(2.0)
    }
}

impl Area for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
}

Para implementar uma característica para um tipo, usamos as palavras-chave impl Trait for Type, em que Trait é o nome da característica que está sendo implementada e Type é o nome do struct ou da enumeração do implementador.

Dentro do bloco impl, colocamos as assinaturas de método que a definição da característica exigia, preenchendo o corpo do método com o comportamento específico que queremos que os métodos da característica tenham para o tipo específico.

Quando um tipo implementa uma determinada característica, ele promete cumprir seu contrato. Após implementar a característica, podemos chamar os métodos em instâncias de Circle e Rectangle, da mesma forma que chamamos métodos regulares, da seguinte maneira:

let circle = Circle { radius: 5.0 };
let rectangle = Rectangle {
    width: 10.0,
    height: 20.0,
};

println!("Circle area: {}", circle.area());
println!("Rectangle area: {}", rectangle.area());

Você pode interagir com esse código neste link do Rust Playground.