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.
Definindo um trait
Seção intitulada “Definindo um trait”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.
Implementando um trait
Seção intitulada “Implementando um trait”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!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”.
Traits como parâmetros de função
Seção intitulada “Traits como parâmetros de função”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);}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.
Traits que você já usou
Seção intitulada “Traits que você já usou”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).
Implementações padrão
Seção intitulada “Implementações padrão”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.
Prática
Seção intitulada “Prática”1. Formas com área
Seção intitulada “1. Formas com área”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: 12Quadrado: 252. Uma função para vários tipos
Seção intitulada “2. Uma função para vários tipos”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 HobbitFilme: Up (96 min)3. Usar ou substituir o padrão
Seção intitulada “3. Usar ou substituir o padrão”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!- Um trait define comportamentos que um tipo pode implementar, parecido com uma interface.
impl NomeDoTrait for Tipofornece a implementação.&impl NomeDoTraitcomo 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,CloneePartialEqsão traits comuns da biblioteca padrão, e#[derive(...)]os implementa automaticamente.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”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
Para saber mais
Seção intitulada “Para saber mais”- Traits: Defining Shared Behavior — The Rust Book (em inglês): o capítulo oficial sobre como definir traits, métodos padrão, parâmetros
impl Traite trait bounds. - Traits — Rust by Example (em inglês): um exemplo executável com um trait, um método padrão e uma implementação.
- Derive — Rust by Example (em inglês): quais traits da biblioteca padrão o
#[derive(...)]consegue implementar para você, com exemplos. - Advanced Traits — The Rust Book (em inglês): o próximo passo, com tipos associados, supertraits e o padrão newtype.
- Traits — The Rust Reference (em inglês): as regras precisas para definir traits, para quando você quiser todos os detalhes.