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: imprimindo para desenvolvedores
Seção intitulada “Debug: imprimindo para desenvolvedores”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,}A macro dbg!, que você pode encontrar em outros códigos, também usa Debug.
Display: imprimindo para usuários
Seção intitulada “Display: imprimindo para usuários”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 HerbertEsse texto tem 21 caracteres(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.
Clone e Copy
Seção intitulada “Clone e Copy”Os dois criam uma duplicata de um valor, mas de jeitos diferentes.
Clonedá 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 umaString.Copycria 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,chare 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" }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 e Eq: comparando com ==
Seção intitulada “PartialEq e Eq: comparando com ==”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: truea != c: trueE 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 e Ord: comparando com < e >
Seção intitulada “PartialOrd e Ord: comparando com < e >”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: true1.2.01.10.32.0.0Como 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: um valor inicial sensato
Seção intitulada “Default: um valor inicial sensato”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: "" }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 e Into: convertendo entre tipos
Seção intitulada “From e Into: convertendo entre tipos”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°FA temperatura do corpo é de 98.6°FCom .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 }))A página de Erros personalizados desenvolve essa ideia com mais detalhes.
Sobrecarga de operadores com Add
Seção intitulada “Sobrecarga de operadores com Add”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 }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.
Quais traits devo derivar?
Seção intitulada “Quais traits devo derivar?”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.
Erros comuns
Seção intitulada “Erros comuns”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 expansion2 | 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 expansion2 | 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.
Prática
Seção intitulada “Prática”1. Ordene uma lista de tarefas
Seção intitulada “1. Ordene uma lista de tarefas”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? trueTask { priority: 1, title: "Pagar o aluguel" }Task { priority: 2, title: "Responder ao Sam" }Task { priority: 3, title: "Regar as plantas" }2. Dinheiro que imprime e soma
Seção intitulada “2. Dinheiro que imprime e soma”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,50Bolo: R$ 4,05Total: R$ 7,55Debug({:?}) é para desenvolvedores e quase sempre é derivado.Display({}) é para usuários e é escrito à mão.Clonecopia quando você pede, com.clone().Copycopia automaticamente, mas só para tipos simples.PartialEq/Eqdão a você o==.PartialOrd/Orddão o<e a ordenação, comparando os campos de cima para baixo.Defaultcria um valor inicial;..Default::default()preenche os campos que você não definiu.- Implemente
Frome você ganhaIntode graça. O operador?usaFrompara 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
Seção intitulada “Teste o que aprendeu”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
Para saber mais
Seção intitulada “Para saber mais”- Derivable Traits — The Rust Book (Appendix C) (em inglês): todos os traits que você pode usar com
#[derive], o que a versão derivada faz e quando vale a pena usá-la. std::fmt: formatação e Display (em inglês): todas as opções de formatação, como largura, preenchimento e{:#?}, e comoDisplayeDebugse encaixam nisso.FromeInto— Rust by Example (em inglês): exemplos curtos e executáveis de conversão entre os seus próprios tipos.std::ops: traits de operadores (em inglês): a lista completa dos operadores que você pode sobrecarregar, doAddaoIndex.std::cmp: traits de comparação (em inglês):PartialEq,Eq,PartialOrdeOrdem detalhes, com as regras que cada um promete cumprir.