Traits Avançadas
Abordamos traits pela primeira vez em “Definindo comportamento compartilhado com traits” no Capítulo 10, mas não discutimos os detalhes mais avançados. Agora que você sabe mais sobre Rust, podemos entrar no âmago da questão.
Definindo Traits com Tipos Associados
Tipos associados conectam um espaço reservado de tipo a uma trait, de modo que as definições de métodos da trait possam usar esses tipos de espaço reservado em suas assinaturas. O implementador de uma trait especificará o tipo concreto a ser usado no lugar do espaço reservado para a implementação específica. Dessa forma, podemos definir uma trait que usa alguns tipos sem precisar saber exatamente quais são esses tipos até que a trait seja implementada.
Descrevemos a maioria dos recursos avançados deste capítulo como raramente necessários. Os tipos associados ficam em algum lugar no meio: são usados com menos frequência do que os recursos explicados no restante do livro, mas com mais frequência do que muitos dos outros recursos discutidos aqui.
Um exemplo de trait com tipo associado é a trait Iterator, fornecida pela
biblioteca padrão. O tipo associado chama-se Item e representa o tipo dos
valores sobre os quais o tipo que implementa a trait Iterator está iterando.
A definição da trait Iterator é mostrada na Listagem 20-13.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Iterator, que possui um tipo associado ItemO tipo Item é um espaço reservado, e a definição do método next mostra que
ele retornará valores do tipo Option<Self::Item>. Implementadores da trait
Iterator especificam o tipo concreto de Item, e o método next retorna um
Option contendo um valor desse tipo concreto.
Os tipos associados podem parecer semelhantes aos genéricos, na medida em que
estes últimos também nos permitem definir comportamento sem especificar, de
antemão, todos os tipos concretos envolvidos. Para examinar a diferença entre
os dois conceitos, veremos uma implementação da trait Iterator em um tipo
chamado Counter, que especifica o tipo Item como u32:
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
Essa sintaxe parece comparável à dos genéricos. Então, por que não definir
a trait Iterator com genéricos, como mostrado na Listagem 20-14?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
Iterator usando genéricosA diferença é que, ao usar genéricos, como na Listagem 20-14, precisamos anotar
os tipos em cada implementação; como também poderíamos implementar
Iterator<String> for Counter, ou qualquer outra variação, poderíamos ter
várias implementações de Iterator para Counter. Em outras palavras, quando
uma trait tem um parâmetro genérico, ela pode ser implementada para um mesmo
tipo várias vezes, alterando os tipos concretos dos parâmetros genéricos a cada
implementação. Quando usássemos o método next em Counter, precisaríamos
fornecer anotações de tipo para indicar qual implementação de Iterator
queremos usar.
Com tipos associados, não precisamos anotar tipos, porque não podemos
implementar a mesma trait várias vezes para um tipo. Na definição da
Listagem 20-13, que usa tipos associados, só podemos escolher o tipo de Item
uma única vez, porque só pode existir um impl Iterator for Counter. Não
precisamos especificar, em todo lugar onde chamamos next em Counter, que
queremos um iterador de valores u32.
Os tipos associados também passam a fazer parte do contrato da trait: implementadores da trait precisam fornecer um tipo para substituir o espaço reservado do tipo associado. Os tipos associados geralmente têm um nome que descreve como o tipo será usado, e documentar o tipo associado na documentação da API é uma boa prática.
Usando Parâmetros Genéricos Padrão e Sobrecarga de Operadores
Quando usamos parâmetros de tipo genérico, podemos especificar um tipo concreto
padrão para o tipo genérico. Isso elimina a necessidade de implementadores da
trait especificarem um tipo concreto se o tipo padrão já servir. Você define um
tipo padrão ao declarar um tipo genérico com a sintaxe
<PlaceholderType=ConcreteType>.
Um ótimo exemplo de situação em que essa técnica é útil é a sobrecarga de
operadores (operator overloading), em que você personaliza o comportamento de
um operador, como +, em situações específicas.
Rust não permite que você crie seus próprios operadores nem que sobrecarregue
operadores arbitrariamente. Mas você pode sobrecarregar as operações e as
traits correspondentes listadas em std::ops implementando a trait associada
ao operador. Por exemplo, na Listagem 20-15, sobrecarregamos o operador +
para somar duas instâncias de Point. Fazemos isso implementando a trait
Add para a struct Point.
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
Add para sobrecarregar o operador + para instâncias de PointO método add soma os valores x de duas instâncias de Point e os valores
y dessas duas instâncias para criar um novo Point. A trait Add possui um
tipo associado chamado Output, que determina o tipo retornado pelo método
add.
O tipo genérico padrão neste código está na trait Add. Eis sua
definição:
#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
}
Esse código deve parecer familiar em termos gerais: uma trait com um método e
um tipo associado. A parte nova é Rhs=Self: essa sintaxe é chamada de
default type parameters. O parâmetro de tipo genérico Rhs, abreviação de
“right-hand side”, define o tipo do parâmetro rhs no método add. Se não
especificarmos um tipo concreto para Rhs ao implementar a trait Add, o tipo
de Rhs terá como padrão Self, que será o tipo sobre o qual estamos
implementando Add.
Quando implementamos Add para Point, usamos o valor padrão de Rhs porque
queríamos somar duas instâncias de Point. Vejamos agora um exemplo de
implementação da trait Add em que queremos personalizar o tipo Rhs, em vez
de usar o padrão.
Temos duas structs, Millimeters e Meters, contendo valores em unidades
diferentes. Esse empacotamento fino de um tipo existente em outra struct é
conhecido como newtype pattern, que descrevemos em mais detalhes na seção
“Implementando traits externas com o padrão newtype”.
Queremos somar valores em milímetros a valores em metros e fazer com que a
implementação de Add realize a conversão corretamente. Podemos implementar
Add para Millimeters, usando Meters como Rhs, como mostra a
Listagem 20-16.
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
Add em Millimeters para somar Millimeters e MetersPara somar Millimeters e Meters, especificamos impl Add<Meters> para
definir o valor do parâmetro de tipo Rhs, em vez de usar o padrão Self.
Você usará parâmetros de tipo padrão de duas maneiras principais:
- Para estender um tipo sem quebrar código existente
- Para permitir personalização em casos específicos dos quais a maioria dos usuários não precisará
A trait Add da biblioteca padrão é um exemplo do segundo propósito:
normalmente, você somará dois tipos iguais, mas a trait Add oferece a
capacidade de ir além disso. Usar um parâmetro de tipo padrão na definição de
Add significa que você não precisa especificar o parâmetro extra na maior
parte do tempo. Em outras palavras, evita-se um pouco de boilerplate de
implementação, o que torna a trait mais fácil de usar.
O primeiro propósito é parecido com o segundo, mas no sentido inverso: se você quiser adicionar um parâmetro de tipo a uma trait existente, pode fornecer um valor padrão para permitir a extensão da funcionalidade da trait sem quebrar o código de implementação já existente.
Desambiguando entre Métodos com o Mesmo Nome
Nada em Rust impede que uma trait tenha um método com o mesmo nome que o método de outra trait, nem impede que você implemente ambas as traits em um mesmo tipo. Também é possível implementar diretamente no tipo um método com o mesmo nome dos métodos dessas traits.
Ao chamar métodos com o mesmo nome, você precisará informar ao Rust qual deles
deseja usar. Considere o código da Listagem 20-17, em que definimos duas traits,
Pilot e Wizard, ambas com um método chamado fly. Em seguida, implementamos
as duas traits para um tipo Human, que já possui um método chamado fly
implementado diretamente. Cada método fly faz algo diferente.
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {}
fly e implementadas no tipo Human, e um método fly também é implementado diretamente em HumanQuando chamamos fly em uma instância de Human, o compilador, por padrão, chama
o método implementado diretamente no tipo, como mostrado na Listagem 20-18.
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
person.fly();
}
fly em uma instância de HumanA execução desse código imprimirá *waving arms furiously*, mostrando que Rust
chamou o método fly implementado diretamente em Human.
Para chamar os métodos fly da trait Pilot ou da trait Wizard,
precisamos usar uma sintaxe mais explícita para especificar a qual método fly
nos referimos. A Listagem 20-19 demonstra essa sintaxe.
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
fly de trait queremos chamarEspecificar o nome da trait antes do nome do método deixa claro para Rust qual
implementação de fly queremos chamar. Também poderíamos escrever
Human::fly(&person), o que é equivalente a person.fly(), usado
na Listagem 20-19, mas isso é um pouco mais longo quando não precisamos
desambiguar.
A execução deste código imprime o seguinte:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
Como o método fly usa um parâmetro self, se tivéssemos dois tipos que
implementassem a mesma trait, Rust poderia descobrir qual implementação
usar com base no tipo de self.
No entanto, funções associadas que não são métodos não possuem parâmetro self.
Quando existem vários tipos ou traits que definem funções associadas com o
mesmo nome, Rust nem sempre sabe a qual tipo você está se referindo, a menos
que use sintaxe totalmente qualificada. Por exemplo, na Listagem 20-20, criamos
uma trait para um abrigo de animais que quer chamar todos os filhotes de
cachorro de Spot. Criamos uma trait Animal com uma função associada chamada
baby_name. A trait Animal é implementada para a struct Dog, na qual também
fornecemos diretamente uma função associada baby_name.
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}
Implementamos em Dog o código para nomear todos os filhotes como Spot, na função
associada baby_name. O tipo Dog também implementa a trait
Animal, que descreve características compartilhadas por todos os animais. Filhotes
de cachorro são chamados de puppy, e isso é expresso na implementação da trait
Animal para Dog, na função associada baby_name dessa trait.
Em main, chamamos a função Dog::baby_name, que invoca a
função definida diretamente em Dog. Esse código imprime o seguinte:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot
Essa saída não é o que queríamos. Queremos chamar a função baby_name que
faz parte da trait Animal implementada em Dog, para que o código
imprima A baby dog is called a puppy. A técnica de especificar o nome da trait,
que usamos na Listagem 20-19, não ajuda aqui; se mudarmos main para
o código da Listagem 20-21, obteremos um erro de compilação.
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
baby_name da trait Animal, mas o Rust não sabe qual implementação usarComo Animal::baby_name não possui um parâmetro self e pode haver
outros tipos que implementem a trait Animal, Rust não consegue descobrir qual
implementação de Animal::baby_name queremos. Obteremos este erro do compilador:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error
Para desambiguar e dizer ao Rust que queremos usar a implementação de
Animal para Dog, e não a implementação de Animal para algum outro
tipo, precisamos usar sintaxe totalmente qualificada. A Listagem 20-22 mostra como
usar essa sintaxe.
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
baby_name da trait Animal como implementada em DogEstamos fornecendo ao Rust uma anotação de tipo entre sinais de menor e maior,
que indica que queremos chamar a função baby_name da trait Animal como
implementada em Dog, ou seja, queremos tratar o tipo Dog como um
Animal nessa chamada de função. Esse código agora imprimirá o que queremos:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
Em geral, a sintaxe totalmente qualificada é definida da seguinte forma:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
Para funções associadas que não são métodos, não haveria receiver: haveria
apenas a lista dos outros argumentos. Você poderia usar sintaxe totalmente
qualificada em todos os lugares em que chama funções ou métodos. No entanto,
pode omitir qualquer parte dessa sintaxe que Rust consiga deduzir a partir de
outras informações do programa. Você só precisa usar essa forma mais detalhada
quando existem várias implementações com o mesmo nome e Rust precisa de ajuda
para identificar qual delas você quer chamar.
Usando Supertraits
Às vezes você pode escrever uma definição de trait que depende de outra trait. Para um tipo implementar a primeira trait, você quer exigir que esse tipo também implemente a segunda. Você faz isso para que a definição da sua trait possa usar os itens associados da segunda. A trait da qual a sua definição depende é chamada de supertrait da sua trait.
Por exemplo, digamos que queremos criar uma trait OutlinePrint com um
método outline_print que imprima um determinado valor formatado de modo a ficar
emoldurado por asteriscos. Ou seja, dada uma struct Point que implementa a
trait Display da biblioteca padrão e produz (x, y), quando chamamos
outline_print em uma instância de Point que tenha 1 em x e 3 em y, ela
deve imprimir o seguinte:
**********
* *
* (1, 3) *
* *
**********
Na implementação do método outline_print, queremos usar a
funcionalidade da trait Display. Portanto, precisamos especificar que a
trait OutlinePrint funcionará apenas para tipos que também implementem Display e
forneçam a funcionalidade de que OutlinePrint precisa. Podemos fazer isso na
definição da trait, especificando OutlinePrint: Display. Essa técnica é
semelhante a adicionar uma trait bound. A Listagem 20-23 mostra uma
implementação da trait OutlinePrint.
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
fn main() {}
OutlinePrint, que exige a funcionalidade de DisplayComo especificamos que OutlinePrint exige a trait Display,
podemos usar a função to_string, implementada automaticamente para qualquer tipo
que implemente Display. Se tentássemos usar to_string sem adicionar os
dois-pontos e especificar a trait Display após o nome da trait, obteríamos um
erro dizendo que nenhum método chamado to_string foi encontrado para o tipo &Self
no escopo atual.
Vamos ver o que acontece quando tentamos implementar OutlinePrint em um tipo que
não implementa Display, como a struct Point:
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
Recebemos um erro dizendo que a trait Display é exigida, mas não foi implementada:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:23
|
20 | impl OutlinePrint for Point {}
| ^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
|
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:24:7
|
24 | p.outline_print();
| ^^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
|
note: required by a bound in `OutlinePrint::outline_print`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
4 | fn outline_print(&self) {
| ------------- required by a bound in this associated function
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors
Para corrigir isso, implementamos Display para Point e satisfazemos a
restrição exigida por OutlinePrint, assim:
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
Então, implementar a trait OutlinePrint para Point compilará
com sucesso, e poderemos chamar outline_print em uma instância de Point para
exibi-la dentro de um contorno de asteriscos.
Implementando Traits Externas com o Padrão Newtype
Na seção “Implementando uma trait em um tipo” do Capítulo 10, mencionamos a regra órfã, que afirma que só podemos implementar uma trait para um tipo se a trait, o tipo, ou ambos, forem locais ao nosso crate. É possível contornar essa restrição usando o padrão newtype, que envolve criar um novo tipo em uma tuple struct. (Cobrimos tuple structs na seção “Criando diferentes tipos com tuple structs”, no Capítulo 5.) A tuple struct terá um campo e será um wrapper fino em torno do tipo para o qual queremos implementar uma trait. Assim, o tipo wrapper é local ao nosso crate, e podemos implementar a trait nele. Newtype é um termo que vem da linguagem de programação Haskell. Não há penalidade de desempenho em tempo de execução ao usar esse padrão, e o tipo wrapper é eliminado em tempo de compilação.
Por exemplo, digamos que queremos implementar Display para Vec<T>, algo que a
regra órfã nos impede de fazer diretamente porque tanto a trait Display quanto o
tipo Vec<T> são definidos fora do nosso crate. Podemos criar uma struct Wrapper
que contenha uma instância de Vec<T>; então, podemos implementar Display para
Wrapper e usar o valor Vec<T>, como mostrado na Listagem 20-24.
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {w}");
}
Wrapper em torno de Vec<String> para implementar DisplayA implementação de Display usa self.0 para acessar o Vec<T> interno
porque Wrapper é uma tuple struct e Vec<T> é o item no índice 0 da
tupla. Assim, podemos usar a funcionalidade da trait Display em Wrapper.
A desvantagem de usar essa técnica é que Wrapper é um novo tipo e, portanto,
não tem os métodos do valor que está encapsulando. Teríamos de implementar
todos os métodos de Vec<T> diretamente em Wrapper de modo que eles
delegassem para self.0, o que nos permitiria tratar Wrapper exatamente como um
Vec<T>. Se quiséssemos que o novo tipo tivesse todos os métodos do tipo interno,
implementar a trait Deref para Wrapper, retornando o tipo interno, seria
uma solução (discutimos a implementação de Deref na seção “Tratando
smart pointers como referências normais”
do Capítulo 15). Se não quiséssemos que Wrapper tivesse todos os
métodos do tipo interno, por exemplo, para restringir seu
comportamento, teríamos de implementar manualmente apenas os métodos desejados.
Esse padrão newtype também é útil mesmo quando traits não estão envolvidas. Vamos mudar o foco e ver algumas maneiras avançadas de interagir com o sistema de tipos de Rust.