Traits avançados em Rust
Na página de Traits, você aprendeu a definir um trait (característica) e a implementá-lo. Nas páginas de Genéricos e Traits comuns, usou traits como Display, From e Add. Isso basta para a maior parte do código do dia a dia. Mas, quando você lê a biblioteca padrão ou um crate popular, aparecem recursos de traits que parecem meio estranhos no começo: type Item, Add<Rhs = Self>, trait Pretty: Display, impl<T: Display> ... for T.
Esta página explica esses recursos um de cada vez. Nenhum deles é mágica. Cada um resolve um problema pequeno e real e, quando você entende o problema, a sintaxe passa a fazer sentido. Vá com calma: tudo bem ler uma seção, testar o código e voltar depois.
Tipos associados: uma resposta por tipo
Seção intitulada “Tipos associados: uma resposta por tipo”Você já conheceu um tipo associado: o Iterator tem type Item. Um tipo associado é um tipo “reservado” dentro de um trait. Cada implementação o preenche uma única vez.
Aqui está um trait Container pequeno. Uma playlist guarda Strings e uma lista de notas guarda u32s, então cada uma escolhe o seu próprio Item:
use std::fmt::Debug;
trait Container { type Item;
fn get(&self, index: usize) -> Option<&Self::Item>; fn size(&self) -> usize;
fn first(&self) -> Option<&Self::Item> { self.get(0) }}
struct Playlist { songs: Vec<String>,}
struct Scores { values: Vec<u32>,}
impl Container for Playlist { type Item = String;
fn get(&self, index: usize) -> Option<&String> { self.songs.get(index) } fn size(&self) -> usize { self.songs.len() }}
impl Container for Scores { type Item = u32;
fn get(&self, index: usize) -> Option<&u32> { self.values.get(index) } fn size(&self) -> usize { self.values.len() }}
// Funciona com qualquer Container cujos itens possam ser impressos com {:?}fn describe<C: Container>(c: &C)where C::Item: Debug,{ println!("{} itens, o primeiro é {:?}", c.size(), c.first());}
fn main() { let playlist = Playlist { songs: vec![String::from("Intro"), String::from("Outro")] }; let scores = Scores { values: vec![90, 72, 85] };
describe(&playlist); describe(&scores);}Saída:
2 itens, o primeiro é Some("Intro")3 itens, o primeiro é Some(90)Dentro do trait, Self::Item quer dizer “o tipo que esta implementação escolheu, seja qual for”. Em describe, C::Item quer dizer a mesma coisa para o tipo C. (A linha where é só um lugar mais organizado para escrever o bound; falamos mais disso no fim da página.)
Por que não usar um parâmetro genérico? Você poderia escrever trait Container<T>. Só que aí um mesmo tipo poderia implementá-lo várias vezes, Container<String> e Container<u32>, e todo mundo que chamasse teria de dizer qual das duas quer. Para uma playlist isso não faz sentido: ela guarda músicas, e ponto.
É exatamente por isso que o Iterator usa type Item. Um iterador de Vec<i32> produz i32 e nada mais, então for x in v.iter() nunca precisa de anotação de tipo. Uma regra prática simples:
- Uma resposta natural por tipo → tipo associado (
Iterator::Item,Add::Output). - Várias respostas válidas por tipo → parâmetro genérico (
From<T>: dá para criar umaStringa partir de um&str, de umchar, de umBox<str>…).
Parâmetros de tipo padrão e operadores
Seção intitulada “Parâmetros de tipo padrão e operadores”Na página de Traits comuns, você fez o + funcionar para um Point implementando Add. Veja como o Add é definido de verdade na biblioteca padrão:
trait Add<Rhs = Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output;}Dois recursos se encontram aqui. Output é um tipo associado (o que o + produz). E Rhs = Self é um parâmetro de tipo padrão: “o lado direito é do mesmo tipo que o esquerdo, a menos que você diga o contrário”. Quando você escreve impl Add for Point, fica com o padrão. Quando quer somar um tipo diferente, escreve impl Add<OutroTipo> for Point.
use std::ops::Add;
#[derive(Debug, Clone, Copy)]struct Meters(f64);
#[derive(Debug, Clone, Copy)]struct Centimeters(f64);
// Usa o padrão Rhs = Self, então isto é Meters + Metersimpl Add for Meters { type Output = Meters; fn add(self, other: Meters) -> Meters { Meters(self.0 + other.0) }}
// Substitui o padrão: Meters + Centimetersimpl Add<Centimeters> for Meters { type Output = Meters; fn add(self, other: Centimeters) -> Meters { Meters(self.0 + other.0 / 100.0) }}
fn main() { let a = Meters(2.0); let b = Meters(3.5); let c = Centimeters(50.0);
println!("{:?}", a + b); println!("{:?}", a + c);}Saída:
Meters(5.5)Meters(2.5)Repare que o Add usa as duas ferramentas: um parâmetro genérico para o lado direito (várias respostas válidas: dá para somar Meters ou Centimeters) e um tipo associado para o resultado (uma resposta para cada par). Parâmetros de tipo padrão são mais úteis quando você está projetando um trait: eles deixam o caso comum curto sem bloquear o caso raro.
Supertraits: “você também precisa implementar este”
Seção intitulada “Supertraits: “você também precisa implementar este””Às vezes o seu trait só faz sentido se o tipo já souber fazer outra coisa. Um supertrait diz “para implementar o meu trait, você também precisa implementar aquele outro”. Você o escreve com dois-pontos, como um bound:
use std::fmt;
// Tudo que é Pretty também precisa ser Displaytrait Pretty: fmt::Display { fn pretty(&self) -> String { format!("*** {} ***", self) }}
struct Temperature { celsius: f64,}
impl fmt::Display for Temperature { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:.1}°C", self.celsius) }}
impl Pretty for Temperature {}
fn main() { let t = Temperature { celsius: 21.456 }; println!("{}", t); println!("{}", t.pretty());}Saída:
21.5°C*** 21.5°C ***O método padrão pretty pode usar {} em self porque o supertrait garante que o Display existe. Sem o : fmt::Display, o Rust recusaria esse format!, já que não teria como saber que o tipo pode ser impresso. O impl Pretty for Temperature {} é vazio: ele só diz “sim, use o método padrão”.
Exemplos reais da biblioteca padrão: Copy: Clone (todo tipo Copy também precisa ser Clone) e Eq: PartialEq.
Blanket implementations: um impl para muitos tipos
Seção intitulada “Blanket implementations: um impl para muitos tipos”Uma blanket implementation (implementação “cobertor”, que cobre tudo) implementa um trait para todo tipo que cumpre uma condição, de uma vez só. A sintaxe é impl<T: AlgumBound> MeuTrait for T:
use std::fmt::Display;
trait Shout { fn shout(&self) -> String;}
// Todo tipo que implementa Display ganha Shout de graçaimpl<T: Display> Shout for T { fn shout(&self) -> String { format!("{}!", self.to_string().to_uppercase()) }}
fn main() { println!("{}", "olá".shout()); println!("{}", 42.shout()); println!("{}", 'a'.shout()); println!("{}", String::from("rust é divertido").shout());}Saída:
OLÁ!42!A!RUST É DIVERTIDO!Nunca escrevemos impl Shout for i32 nem impl Shout for char. A blanket impl cobriu todos eles, inclusive tipos com Display que ainda nem existem.
Você já usou uma blanket impl várias vezes sem saber. A biblioteca padrão tem (mais ou menos) impl<T: Display> ToString for T. É por isso que todo tipo com Display tem um método .to_string(). O mesmo truque faz você ganhar Into de graça quando implementa From.
Um limite: depois que existe uma blanket impl, você não pode adicionar um segundo impl, mais específico, para um tipo que ela já cobre (por exemplo, um impl Shout for i32 especial). O Rust teria duas respostas e recusa com um erro de “conflicting implementations” (implementações conflitantes).
A regra órfã e o truque do newtype
Seção intitulada “A regra órfã e o truque do newtype”Esta é uma regra que surpreende todo mundo pelo menos uma vez. Tente dar ao Vec<String> um Display bonito:
use std::fmt;
impl fmt::Display for Vec<String> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.join(", ")) }}error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate --> src/main.rs:3:1 |3 | impl fmt::Display for Vec<String> { | ^^^^^^^^^^^^^^^^^^^^^^----------- | | | `Vec` is not defined in the current crate | = note: define and implement a trait or new type insteadO compilador diz que só é possível implementar traits definidos no crate atual para tipos de fora dele. Essa é a regra órfã (orphan rule): você só pode implementar um trait para um tipo se o trait ou o tipo estiver definido no seu crate. O Display pertence à biblioteca padrão, e o Vec também, então nenhum dos dois é seu.
Por que essa regra existe? Imagine que dois crates dos quais você depende escreveram, cada um, o seu próprio impl Display for Vec<String>. Qual deles o Rust deveria usar? Não existe uma boa resposta, então o Rust torna essa situação impossível.
O truque é o padrão newtype: embrulhar o tipo de fora em uma struct minúscula que é sua. Agora o tipo é seu, e você pode implementar o que quiser para ele:
use std::fmt;
// Um invólucro local em volta de um tipo de forastruct Shopping(Vec<String>);
impl fmt::Display for Shopping { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) }}
fn main() { let list = Shopping(vec![String::from("leite"), String::from("ovos"), String::from("pão")]); println!("Você precisa de: {}", list); println!("São {} itens", list.0.len());}Saída:
Você precisa de: [leite, ovos, pão]São 3 itensO self.0 acessa o Vec que está dentro. Um newtype não tem custo nenhum em tempo de execução: na memória, Shopping é exatamente um Vec<String>. A desvantagem é que os métodos do Vec não ficam direto em Shopping, então você escreve list.0.len() (ou adiciona os métodos de que precisar). Newtypes servem para muito mais do que contornar a regra órfã; veja Padrões de projeto.
Sintaxe totalmente qualificada: quando os nomes se chocam
Seção intitulada “Sintaxe totalmente qualificada: quando os nomes se chocam”Dois traits podem ter métodos com o mesmo nome, e o próprio tipo também pode ter um. Qual deles robot.greet() chama?
trait English { fn greet(&self) -> String; fn language() -> String;}
trait Spanish { fn greet(&self) -> String; fn language() -> String;}
struct Robot;
impl Robot { fn greet(&self) -> String { String::from("BEEP BOOP") }}
impl English for Robot { fn greet(&self) -> String { String::from("Hello!") } fn language() -> String { String::from("Inglês") }}
impl Spanish for Robot { fn greet(&self) -> String { String::from("¡Hola!") } fn language() -> String { String::from("Espanhol") }}
fn main() { let r = Robot;
println!("{}", r.greet()); // o método do próprio tipo ganha println!("{}", English::greet(&r)); // escolhe um trait pelo nome println!("{}", Spanish::greet(&r));
// Aqui não há `self`, então o Rust precisa do tipo E do trait: println!("{}", <Robot as English>::language()); println!("{}", <Robot as Spanish>::language());}Saída:
BEEP BOOPHello!¡Hola!InglêsEspanholSão três níveis, do mais curto ao totalmente explícito:
r.greet(): o método do próprio tipo (o método inerente) é escolhido primeiro.English::greet(&r): “a versão doEnglish”. O Rust descobre o tipo a partir de&r.<Robot as English>::language(): sintaxe totalmente qualificada. Você precisa dela para funções semself, porque não há nenhum valor para o Rust olhar.English::language()sozinho falha comerror[E0790]: cannot call associated function on trait without specifying the corresponding impl type, ou seja, o Rust não sabe de qual tipo é a implementação que você quer.
Você não vai precisar disso com frequência. Quando precisar, a mensagem de erro do compilador geralmente sugere a correção exata.
impl Trait em argumentos e retornos, e o where
Seção intitulada “impl Trait em argumentos e retornos, e o where”Você pode escrever impl Trait em dois lugares, e o significado é um pouco diferente em cada um:
- Na posição de argumento,
fn f(x: impl Trait): quem chama escolhe o tipo. É um jeito curto de escrever a função genéricafn f<T: Trait>(x: T). - Na posição de retorno,
fn f() -> impl Trait: a função escolhe um tipo concreto e o mantém escondido. Quem chama só sabe que “é alguma coisa que implementaTrait”.
// Quem chama escolhe: qualquer iterador de u32 servefn print_all(items: impl Iterator<Item = u32>) { let all: Vec<u32> = items.collect(); println!("{:?}", all);}
// A função escolhe: o tipo real é um Filter<...> enorme que nunca escrevemosfn evens_up_to(limit: u32) -> impl Iterator<Item = u32> { (0..=limit).filter(|n| n % 2 == 0)}
fn main() { print_all(vec![3, 1, 4].into_iter()); print_all(evens_up_to(10));}Saída:
[3, 1, 4][0, 2, 4, 6, 8, 10]O impl Trait na posição de retorno é especialmente útil com closures e iteradores. O tipo real do resultado de evens_up_to inclui o tipo da closure, e closures não têm um nome que você consiga digitar. Com impl Iterator, você pula essa parte.
Quando uma função tem vários parâmetros genéricos, cada um com vários bounds, os sinais de < e > ficam lotados. Uma cláusula where move os bounds para depois da assinatura, e o formato da função fica mais fácil de ler:
use std::fmt::{Debug, Display};
// Lotado: fn report<T: Display, U: Debug + PartialOrd>(label: T, a: U, b: U) -> String// Organizado:fn report<T, U>(label: T, a: U, b: U) -> Stringwhere T: Display, U: Debug + PartialOrd,{ let bigger = if a > b { a } else { b }; format!("{}: o maior é {:?}", label, bigger)}
fn main() { println!("{}", report("notas", 7, 12)); println!("{}", report(String::from("nomes"), "ann", "bob"));}Saída:
notas: o maior é 12nomes: o maior é "bob"Para o compilador, as duas formas significam exatamente a mesma coisa. O where também é o lugar natural para bounds em tipos associados, como o C::Item: Debug do primeiro exemplo desta página.
Erros comuns
Seção intitulada “Erros comuns”Implementar um trait e esquecer o supertrait dele. Se você remover o bloco impl fmt::Display for Temperature do exemplo de supertrait, recebe:
error[E0277]: `Temperature` doesn't implement `std::fmt::Display` --> src/main.rs:13:17 |13 | impl Pretty for Temperature {} | ^^^^^^^^^^^ unsatisfied trait bound |note: required by a bound in `Pretty` --> src/main.rs:3:15 | 3 | trait Pretty: fmt::Display { | ^^^^^^^^^^^^ required by this bound in `Pretty`O erro diz que Temperature não implementa Display, e a nota aponta direto para o : fmt::Display que exige isso. A correção é implementar o supertrait (aqui, o Display) também.
Devolver dois tipos diferentes em -> impl Trait. O impl Trait esconde um tipo. Ele não pode ser “às vezes este, às vezes aquele”:
fn numbers(evens: bool) -> impl Iterator<Item = u32> { if evens { (0..10).filter(|n| n % 2 == 0) } else { vec![1, 3, 5].into_iter() }}error[E0308]: `if` and `else` have incompatible types --> src/main.rs:5:9 |2 | / if evens {3 | | (0..10).filter(|n| n % 2 == 0) | | ------------------------------ | | | | | | | the expected closure | | expected because of this4 | | } else {5 | | vec![1, 3, 5].into_iter() | | ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Filter<Range<{integer}>, {closure@...}>`, found `IntoIter<{integer}>`6 | | } | |_____- `if` and `else` have incompatible types | = note: expected struct `Filter<std::ops::Range<{integer}>, {closure@src/main.rs:3:24: 3:27}>` found struct `std::vec::IntoIter<{integer}>`help: you could change the return type to be a boxed trait object |1 - fn numbers(evens: bool) -> impl Iterator<Item = u32> {1 + fn numbers(evens: bool) -> Box<dyn Iterator<Item = u32>> { |help: if you change the return type to expect trait objects, box the returned expressions |3 ~ Box::new((0..10).filter(|n| n % 2 == 0))4 | } else {5 ~ Box::new(vec![1, 3, 5].into_iter()) |O compilador reclama que o if e o else têm tipos incompatíveis. Quando o tipo muda de verdade em tempo de execução, devolva um Box<dyn Trait>, como o próprio compilador sugere. Veja Trait objects.
- Um tipo associado (
type Item) dá uma resposta por implementação. Use um parâmetro genérico quando um tipo puder implementar o trait de várias formas. - Parâmetros de tipo padrão, como
Add<Rhs = Self>, deixam o caso comum curto;impl Add<Outro> for Tsubstitui o padrão. - Um supertrait (
trait Pretty: Display) exige outro trait e deixa os métodos padrão usarem esse trait. - Uma blanket impl (
impl<T: Display> MeuTrait for T) cobre de uma vez todos os tipos que se encaixam. É assim que o.to_string()funciona. - A regra órfã impede que você implemente um trait de fora para um tipo de fora. Embrulhe o tipo em um newtype para contornar isso.
- Use a sintaxe totalmente qualificada
<Tipo as Trait>::funcao()quando os nomes se chocarem;impl Traitna posição de retorno esconde um único tipo concreto;wheredeixa bounds longos legíveis.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Por que o Iterator usa type Item em vez de Iterator<T>?
2. O que Rhs = Self quer dizer em trait Add<Rhs = Self>?
3. Você quer impl Display for Vec<String>. O Rust recusa. Qual é a correção mais comum?
4. Qual linha chama a versão do English de uma função sem o parâmetro self?
5. Em fn evens() -> impl Iterator<Item = u32>, quem decide o tipo concreto do iterador?
Pontuação: 0 / 5
Para saber mais
Seção intitulada “Para saber mais”- Advanced Traits — The Rust Book (em inglês): tipos associados, parâmetros de tipo padrão, supertraits e o padrão newtype, com mais exemplos.
- Traits — The Rust Reference (em inglês): as regras precisas para itens de traits, supertraits e itens associados.
- Orphan rules — The Rust Reference (em inglês): a definição exata de quando um impl é permitido.
impl Trait— The Rust Reference (em inglês): a diferença entreimpl Traitna posição de argumento e na posição de retorno.- Supertraits — Rust by Example (em inglês): um exemplo curto e executável de traits construídos em cima de outros traits.