Pular para o conteúdo

Traits em Rust

Um trait (algo como “característica”) define um conjunto de comportamentos que um tipo pode implementar, parecido com uma interface em outras linguagens. É assim que o Rust descreve “este tipo sabe fazer X”, e é isso que está por trás das trait bounds que você viu na página de Genéricos.

trait Speak {
fn speak(&self) -> String;
}

Isso quer dizer: qualquer tipo que implemente Speak precisa oferecer um método speak que devolve uma String. Sozinho, um trait é só uma promessa. Ele não faz nada até que algum tipo o implemente.

trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String {
String::from("Au au!")
}
}
impl Speak for Cat {
fn speak(&self) -> String {
String::from("Miau!")
}
}
fn main() {
let dog = Dog;
let cat = Cat;
println!("{}", dog.speak());
println!("{}", cat.speak());
}

Saída:

Au au!
Miau!
▶ Experimente você mesmo

Dog e Cat são tipos sem nenhuma relação entre si, mas os dois implementam Speak, então dá para chamar .speak() em ambos. É o mesmo padrão dos blocos impl da página de Structs: impl Speak for Dog só diz “é assim que Dog cumpre o trait Speak”.

Aqui está a grande vantagem: você pode escrever uma função que aceita qualquer tipo que implemente um trait, sem se importar com qual tipo específico ele é:

trait Speak {
fn speak(&self) -> String;
}
struct Dog;
impl Speak for Dog {
fn speak(&self) -> String { String::from("Au au!") }
}
fn announce(animal: &impl Speak) {
println!("O animal diz: {}", animal.speak());
}
fn main() {
let dog = Dog;
announce(&dog);
}
▶ Experimente você mesmo

animal: &impl Speak quer dizer “qualquer tipo que implemente Speak; não preciso saber qual”. Se você criar um tipo Bird que também implemente Speak, announce funciona com ele também, sem nenhuma mudança.

Lembra do #[derive(Debug)] da página de Structs? O Debug também é um trait. O derive só gera a implementação automaticamente, para você não precisar escrevê-la à mão. Outros traits comuns que você vai encontrar: Clone (para .clone()), PartialEq (para ==) e PartialOrd (para < e >, que você viu sendo usado como trait bound na página de Genéricos).

Um trait pode oferecer um corpo padrão para um método. Os tipos que o implementam podem usar esse corpo como está ou substituí-lo:

trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("Olá, {}!", self.name())
}
}

Qualquer tipo que implemente Greet só precisa definir name(). O greet() vem de graça, a menos que o tipo prefira substituí-lo também.

Os traits vão além do que esta página mostra. A página de Traits comuns apresenta os traits da biblioteca padrão que você vai usar todo dia, e a de Traits avançados cobre tipos associados, supertraits e outras ferramentas que aparecem em projetos maiores.

Defina um trait Area com um único método, fn area(&self) -> f64. Implemente-o para um Rectangle (com width e height) e para um Square (com side). Imprima a área de um retângulo de 3 por 4 e de um quadrado de lado 5. Dica: cada bloco impl Area for ... só precisa desse único método, e o corpo dele é uma simples multiplicação.

Ver solução
trait Area {
fn area(&self) -> f64;
}
struct Rectangle {
width: f64,
height: f64,
}
struct Square {
side: f64,
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
impl Area for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
fn main() {
let rect = Rectangle { width: 3.0, height: 4.0 };
let square = Square { side: 5.0 };
println!("Retângulo: {}", rect.area());
println!("Quadrado: {}", square.area());
}

Saída:

Retângulo: 12
Quadrado: 25
▶ Experimente você mesmo

Defina um trait Describe com fn describe(&self) -> String. Implemente-o para um Book (com um title) e para um Movie (com title e minutes). Depois, escreva uma única função fn show(item: &impl Describe) que imprime a descrição e chame-a com os dois. Dica: use format! para montar cada texto de descrição.

Ver solução
trait Describe {
fn describe(&self) -> String;
}
struct Book {
title: String,
}
struct Movie {
title: String,
minutes: u32,
}
impl Describe for Book {
fn describe(&self) -> String {
format!("Livro: {}", self.title)
}
}
impl Describe for Movie {
fn describe(&self) -> String {
format!("Filme: {} ({} min)", self.title, self.minutes)
}
}
fn show(item: &impl Describe) {
println!("{}", item.describe());
}
fn main() {
let book = Book { title: String::from("O Hobbit") };
let movie = Movie { title: String::from("Up"), minutes: 96 };
show(&book);
show(&movie);
}

Saída:

Livro: O Hobbit
Filme: Up (96 min)
▶ Experimente você mesmo

Pegue o trait Greet desta página, com o método padrão greet(). Implemente-o para um Person, que fica com o padrão, e para um Pirate, que substitui o greet() para dizer Ahoy, ...!. Dica: os dois tipos precisam definir name(). Só o Pirate escreve também o seu próprio greet().

Ver solução
trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("Olá, {}!", self.name())
}
}
struct Person {
name: String,
}
struct Pirate {
name: String,
}
impl Greet for Person {
fn name(&self) -> String {
self.name.clone()
}
}
impl Greet for Pirate {
fn name(&self) -> String {
self.name.clone()
}
fn greet(&self) -> String {
format!("Ahoy, {}!", self.name())
}
}
fn main() {
let ana = Person { name: String::from("Ana") };
let jack = Pirate { name: String::from("Jack") };
println!("{}", ana.greet());
println!("{}", jack.greet());
}

Saída:

Olá, Ana!
Ahoy, Jack!
▶ Experimente você mesmo
  • Um trait define comportamentos que um tipo pode implementar, parecido com uma interface.
  • impl NomeDoTrait for Tipo fornece a implementação.
  • &impl NomeDoTrait como tipo de parâmetro aceita qualquer tipo que implemente esse trait.
  • Traits podem oferecer corpos padrão para métodos, que quem implementa pode usar ou substituir.
  • Debug, Clone e PartialEq são traits comuns da biblioteca padrão, e #[derive(...)] os implementa automaticamente.

Teste o que aprendeu

1. O que um trait define?

2. O que animal: &impl Speak quer dizer como tipo de parâmetro?

3. O que o #[derive(Debug)] está fazendo de verdade?

4. O que é uma implementação padrão em um trait?

5. Segundo esta página, qual destes é um trait, e não uma struct ou um enum?

Pontuação: 0 / 5