Pular para o conteúdo

Traits comuns em Rust

Na página de Traits, você escreveu os seus próprios traits (características que um tipo pode ter). Mas a biblioteca padrão já vem com um conjunto de traits que quase todo programa Rust usa. Eles respondem a perguntas simples sobre um tipo: Dá para imprimir? Dá para copiar? Dá para comparar? Dá para criar um valor vazio?

Pense neles como broches em um uniforme. Cada broche diz “este tipo sabe fazer X”. Muitos desses broches você ganha de graça com #[derive(...)]. Outros você escreve à mão. Esta página apresenta os que você vai encontrar com mais frequência e termina com uma tabela para consultar depois.

Debug permite imprimir um valor com {:?}. Ele é feito para você, quem está programando: serve para conferir valores enquanto você escreve e corrige o código. Quase nunca é preciso escrevê-lo à mão; basta usar derive. Use {:#?} para uma versão “bonita”, com um campo por linha, que é mais fácil de ler em structs grandes:

#[derive(Debug)]
struct Book {
title: String,
pages: u32,
}
fn main() {
let book = Book { title: String::from("Duna"), pages: 412 };
println!("{:?}", book);
println!("{:#?}", book);
}

Saída:

Book { title: "Duna", pages: 412 }
Book {
title: "Duna",
pages: 412,
}
▶ Experimente você mesmo

A macro dbg!, que você pode encontrar em outros códigos, também usa Debug.

Display é o que o {} usa. Ele serve para texto voltado ao usuário, a mensagem que uma pessoa lê na tela. O Rust não tem como adivinhar como isso deve ficar, então não existe derive. Você mesmo escreve, implementando fmt::Display:

use std::fmt;
struct Book {
title: String,
author: String,
}
impl fmt::Display for Book {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} de {}", self.title, self.author)
}
}
fn main() {
let book = Book {
title: String::from("Duna"),
author: String::from("Frank Herbert"),
};
println!("{}", book);
let text = book.to_string();
println!("Esse texto tem {} caracteres", text.len());
}

Saída:

Duna de Frank Herbert
Esse texto tem 21 caracteres
▶ Experimente você mesmo

(Na verdade, .len() conta bytes, e não caracteres. Aqui os dois números coincidem porque o texto não tem acentos; uma letra como “ç” ou “ã” ocupa 2 bytes.)

write! funciona como println!, mas escreve no formatador f em vez de escrever na tela. Um bônus: assim que um tipo tem Display, você ganha .to_string() de graça. Você vai ver Display de novo na página de Erros personalizados, onde ele dá aos erros uma mensagem amigável.

Os dois criam uma duplicata de um valor, mas de jeitos diferentes.

  • Clone dá a você um método .clone(). Você o chama de propósito, e ele pode fazer um trabalho de verdade, como copiar todo o texto de uma String.
  • Copy cria uma duplicata automaticamente toda vez que você atribui ou passa o valor. Ele só é permitido para tipos pequenos e simples, que podem ser copiados bit a bit: números, bool, char e structs formadas só por esses tipos.
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
#[derive(Debug, Clone)]
struct User {
name: String,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // Copy: p1 continua utilizável
println!("{:?} e {:?}", p1, p2);
let u1 = User { name: String::from("Ana") };
let u2 = u1.clone(); // Clone: pedimos uma cópia
println!("{:?} e {:?}", u1, u2);
}

Saída:

Point { x: 1, y: 2 } e Point { x: 1, y: 2 }
User { name: "Ana" } e User { name: "Ana" }
▶ Experimente você mesmo

Sem Copy, let p2 = p1; faria um move (mover) de p1, e você não poderia mais usá-lo depois (veja Ownership). Repare que Copy sempre precisa de Clone também; é por isso que os dois são derivados juntos.

PartialEq permite usar == e !=. Quando você o deriva, dois valores são iguais se todos os seus campos forem iguais:

#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
let c = Point { x: 5, y: 0 };
println!("a == b: {}", a == b);
println!("a != c: {}", a != c);
}

Saída:

a == b: true
a != c: true
▶ Experimente você mesmo

E o que é Eq? Ele não acrescenta nenhum método novo. É uma promessa: “todo valor é sempre igual a si mesmo”. Parece óbvio, mas os números de ponto flutuante quebram essa regra: em Rust, f64::NAN == f64::NAN é false. Por isso, f64 tem PartialEq, mas não tem Eq. Você precisa de Eq (mais Hash) para usar o seu tipo como chave de um HashMap ou dentro de um HashSet. Se a sua struct não tem campos de ponto flutuante, derive os dois.

PartialOrd permite usar <, >, <= e >=. Ord acrescenta uma ordenação completa, que é o que .sort(), .max() e .min() precisam. Quando você os deriva, o Rust compara os campos de cima para baixo: primeiro o primeiro campo, e só se eles forem iguais ele olha o próximo.

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Version {
major: u32,
minor: u32,
patch: u32,
}
fn main() {
let old = Version { major: 1, minor: 2, patch: 0 };
let new = Version { major: 1, minor: 10, patch: 3 };
println!("old < new: {}", old < new);
let mut versions = vec![
Version { major: 2, minor: 0, patch: 0 },
new,
old,
];
versions.sort();
for v in &versions {
println!("{}.{}.{}", v.major, v.minor, v.patch);
}
}

Saída:

old < new: true
1.2.0
1.10.3
2.0.0
▶ Experimente você mesmo

Como os campos são comparados em ordem, a ordem em que você escreve os campos importa. Aqui major é o mais importante, que é exatamente o certo para números de versão. Em enums, as variantes listadas primeiro contam como “menores”. Assim como acontece com Eq, os números de ponto flutuante só têm PartialOrd, não Ord. A página de Ordenação e busca mostra como ordenar floats mesmo assim.

Default dá ao seu tipo uma função ::default() que cria um valor “inicial”. Quando derivado, cada campo recebe o seu próprio valor padrão: 0 para números, false para bool, uma String vazia, um Vec vazio. A sintaxe ..Default::default() permite definir alguns campos e preencher o resto:

#[derive(Debug, Default)]
struct Settings {
volume: u32,
muted: bool,
username: String,
}
fn main() {
let basic = Settings::default();
println!("{:?}", basic);
let custom = Settings {
volume: 80,
..Default::default()
};
println!("{:?}", custom);
}

Saída:

Settings { volume: 0, muted: false, username: "" }
Settings { volume: 80, muted: false, username: "" }
▶ Experimente você mesmo

Se você quiser valores iniciais diferentes (por exemplo, volume 50), não use o derive e escreva você mesmo um impl Default for Settings, com uma fn default() -> Self que devolve os valores que você quer.

From descreve como construir um tipo a partir de outro. Você já usou: String::from("oi"). Quando você implementa From, o Rust dá a você o Into de graça, no sentido contrário:

struct Celsius(f64);
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
fn main() {
let boiling = Fahrenheit::from(Celsius(100.0));
println!("A água ferve a {}°F", boiling.0);
let body: Fahrenheit = Celsius(37.0).into();
println!("A temperatura do corpo é de {:.1}°F", body.0);
}

Saída:

A água ferve a 212°F
A temperatura do corpo é de 98.6°F
▶ Experimente você mesmo

Com .into(), o Rust precisa saber qual é o tipo de destino, e é por isso que escrevemos let body: Fahrenheit. (Repare também que o Rust usa ponto, e não vírgula, como separador decimal: 98.6.)

Como o ? usa o From. É aqui que o From brilha de verdade. Quando o operador ? encontra um erro, ele chama From::from para transformar esse erro no tipo de erro que a sua função devolve. Então, se você implementar From para o seu próprio tipo de erro, o ? converte os erros para você:

use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
BadNumber(ParseIntError),
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::BadNumber(e)
}
}
fn parse_age(text: &str) -> Result<u32, AppError> {
let age: u32 = text.parse()?; // aqui o ParseIntError vira um AppError
Ok(age)
}
fn main() {
println!("{:?}", parse_age("42"));
println!("{:?}", parse_age("abc"));
}

Saída:

Ok(42)
Err(BadNumber(ParseIntError { kind: InvalidDigit }))
▶ Experimente você mesmo

A página de Erros personalizados desenvolve essa ideia com mais detalhes.

Em Rust, a + b é, na verdade, uma chamada ao método add do trait std::ops::Add. Implemente-o para o seu tipo e o + passa a funcionar. O type Output diz qual tipo a soma produz:

use std::ops::Add;
#[derive(Debug, Clone, Copy, 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() {
let a = Point { x: 1, y: 2 };
let b = Point { x: 3, y: 4 };
println!("{:?}", a + b);
}

Saída:

Point { x: 4, y: 6 }
▶ Experimente você mesmo

A mesma ideia vale para Sub (-), Mul (*), Neg (o - unário) e AddAssign (+=). Use isso quando o significado for óbvio, como somar dois pontos ou duas quantias de dinheiro. Se quem lê o código precisar adivinhar o que + significa para o seu tipo, um método comum com um nome claro é melhor.

Aqui vai um guia rápido para uma struct típica:

Trait Derivar? O que você ganha
Debug Quase sempre Impressão com {:?} e {:#?}
Clone Geralmente .clone()
Copy Só para tipos pequenos, sem campos String, Vec ou Box Cópias automáticas em vez de moves
PartialEq Geralmente == e !=
Eq Quando não há campos de ponto flutuante Necessário para chaves de HashMap (junto com Hash)
Hash Quando o tipo é usado como chave de HashMap ou em um HashSet Cálculo de hash
PartialOrd, Ord Quando os valores têm uma ordem natural <, >, .sort(), .max()
Default Quando um valor “vazio” faz sentido ::default() e ..Default::default()
Display Não dá para derivar; escreva à mão Impressão com {} e .to_string()

Uma primeira linha muito comum é #[derive(Debug, Clone, PartialEq)]. Acrescente outros quando precisar; o compilador avisa quando estiver faltando algum.

Derivar Copy em uma struct que tem uma String.

#[derive(Clone, Copy)]
struct User {
name: String,
age: u32,
}
error[E0204]: the trait `Copy` cannot be implemented for this type
--> src/main.rs:2:8
|
1 | #[derive(Clone, Copy)]
| ---- in this derive macro expansion
2 | struct User {
| ^^^^
3 | name: String,
| ------------ this field does not implement `Copy`

O compilador diz que o trait Copy não pode ser implementado para esse tipo, porque o campo name não implementa Copy. Uma String é dona de memória no heap, então não pode ser copiada bit a bit. Remova o Copy e use .clone() quando precisar de uma duplicata.

Derivar Eq em uma struct que tem um float.

#[derive(Debug, PartialEq, Eq)]
struct Product { name: String, price: f64 }
error[E0277]: the trait bound `f64: Eq` is not satisfied
--> src/main.rs:2:32
|
1 | #[derive(Debug, PartialEq, Eq)]
| -- in this derive macro expansion
2 | struct Product { name: String, price: f64 }
| ^^^^^^^^^^ the trait `Eq` is not implemented for `f64`

Aqui o erro avisa que f64 não implementa o trait Eq. Mantenha o PartialEq e tire o Eq. Ou guarde o preço em centavos inteiros em um u64, o que também é mais seguro para lidar com dinheiro.

Crie uma struct Task com um priority: u8 e um title: String. Derive os traits necessários para conseguir ordenar um Vec<Task>, imprimir cada tarefa com {:?} e comparar duas tarefas com ==. Números menores são mais urgentes, então devem vir primeiro. Dica: o .sort() precisa de Ord, e o Ord também precisa de Eq, PartialEq e PartialOrd. Coloque priority como primeiro campo para que ele seja comparado primeiro.

Ver solução
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Task {
priority: u8,
title: String,
}
fn main() {
let mut tasks = vec![
Task { priority: 3, title: String::from("Regar as plantas") },
Task { priority: 1, title: String::from("Pagar o aluguel") },
Task { priority: 2, title: String::from("Responder ao Sam") },
];
let copy = tasks[1].clone();
println!("Mesma tarefa? {}", copy == tasks[1]);
tasks.sort();
for task in &tasks {
println!("{:?}", task);
}
}

Saída:

Mesma tarefa? true
Task { priority: 1, title: "Pagar o aluguel" }
Task { priority: 2, title: "Responder ao Sam" }
Task { priority: 3, title: "Regar as plantas" }
▶ Experimente você mesmo

Crie uma struct Money que guarda centavos inteiros em cents: u64. Implemente From<u64> para que 350.into() funcione, Display para que o valor seja impresso como R$ 3,50, e Add para que dois valores possam ser somados com +. Dica: os reais são cents / 100 e o resto é cents % 100. Imprima o resto com {:02} para que 5 centavos apareçam como 05. Derive Copy para ainda conseguir imprimir os valores depois de somá-los.

Ver solução
use std::fmt;
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Money {
cents: u64,
}
impl From<u64> for Money {
fn from(cents: u64) -> Self {
Money { cents }
}
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "R$ {},{:02}", self.cents / 100, self.cents % 100)
}
}
impl Add for Money {
type Output = Money;
fn add(self, other: Money) -> Money {
Money { cents: self.cents + other.cents }
}
}
fn main() {
let coffee: Money = 350.into();
let cake = Money::from(405);
let total = coffee + cake;
println!("Café: {coffee}");
println!("Bolo: {cake}");
println!("Total: {total}");
}

Saída:

Café: R$ 3,50
Bolo: R$ 4,05
Total: R$ 7,55
▶ Experimente você mesmo
  • Debug ({:?}) é para desenvolvedores e quase sempre é derivado. Display ({}) é para usuários e é escrito à mão.
  • Clone copia quando você pede, com .clone(). Copy copia automaticamente, mas só para tipos simples.
  • PartialEq/Eq dão a você o ==. PartialOrd/Ord dão o < e a ordenação, comparando os campos de cima para baixo.
  • Default cria um valor inicial; ..Default::default() preenche os campos que você não definiu.
  • Implemente From e você ganha Into de graça. O operador ? usa From para converter erros.
  • Implemente std::ops::Add (e os traits parecidos) para fazer o + funcionar com os seus próprios tipos.

Quer ir mais longe? A página de Traits avançados explica tipos associados como o type Output, parâmetros de tipo padrão e como implementar um trait para um tipo que não foi você que escreveu.

Teste o que aprendeu

1. Qual trait o {} do println! usa?

2. Por que uma struct com um campo String não pode derivar Copy?

3. Quando você deriva PartialOrd, como duas structs são comparadas?

4. Você implementou From<ParseIntError> for AppError. O que isso permite que o ? faça?

5. Qual trait você implementa para fazer a + b funcionar com o seu próprio tipo?

Pontuação: 0 / 5