Formatação e o trait Display
Nesta aula, você aprenderá a formatar saídas em Rust usando a macro format!, as diferenças entre os traits Display e Debug, os especificadores de formatação e como implementar Display para seus próprios tipos. Ao final, terá domínio para gerar saídas claras e customizadas em seus programas.
Bem-vindo à vigésima quinta aula do nosso curso de Rust! Nesta aula, vamos explorar um dos aspectos mais utilizados no dia a dia: a formatação de texto. Em Rust, a formatação é feita principalmente através da macro format!, que permite criar strings formatadas de forma segura e eficiente. Além disso, vamos entender a diferença entre os traits Display e Debug, e como implementar Display para tipos personalizados, garantindo que suas saídas sejam legíveis e bem estruturadas.
A formatação não é apenas uma questão de estética; ela é crucial para logs, mensagens de erro, interfaces de usuário e serialização de dados. Dominar essas técnicas fará com que seu código seja mais expressivo e profissional. Vamos começar!
format!
A macro format! é a base de toda formatação em Rust. Ela recebe uma string de formato e argumentos, retornando uma String formatada. É a forma mais flexível de construir strings dinâmicas, sem a necessidade de concatenação manual.
O primeiro argumento é a string de formato, que contém placeholders (especificadores) entre chaves {}. Os argumentos seguintes são os valores que serão inseridos nos placeholders. A macro é avaliada em tempo de compilação, garantindo que o número de argumentos corresponda aos especificadores, evitando erros em tempo de execução.
fn main() {
let nome = "Alice";
let idade = 30;
let mensagem = format!("Olá, {}! Você tem {} anos.", nome, idade);
println!("{}", mensagem);
}Além de format!, existem outras macros que usam a mesma sintaxe: print!, println!, eprint!, eprintln!, write! e writeln!. Todas compartilham o mesmo sistema de formatação, então aprender format! é aprender todas.
Display vs Debug
Em Rust, dois traits principais controlam a formatação: Display e Debug. O trait Display é usado para formatação voltada ao usuário final, enquanto Debug é usado para depuração e logs internos. Ambos são implementados por tipos primitivos e pela maioria dos tipos da biblioteca padrão.
A diferença crucial está no propósito: Display deve produzir uma representação legível e amigável, enquanto Debug geralmente produz uma representação mais técnica e completa. Por exemplo, para um número, Display mostra apenas o número, enquanto Debug também mostra o número, mas para tipos complexos, Debug pode incluir informações adicionais.
fn main() {
let numero = 42;
println!("Display: {}", numero); // 42
println!("Debug: {:?}", numero); // 42
let vetor = vec![1, 2, 3];
println!("Display: {}", vetor); // Erro! Vec não implementa Display
println!("Debug: {:?}", vetor); // [1, 2, 3]
}Para tipos personalizados, você deve implementar explicitamente esses traits. O Rust não pode derivar Display automaticamente, pois a representação ideal depende do contexto. No entanto, Debug pode ser derivado com #[derive(Debug)], que gera uma implementação padrão.
Especificadores
Os especificadores de formatação permitem controlar como os valores são exibidos. Eles são colocados dentro das chaves {} e podem incluir: posição, nome, preenchimento, alinhamento, largura, precisão e tipo de formatação.
Vamos ver alguns exemplos práticos:
fn main() {
// Preenchimento com zeros à esquerda
println!("{:05}", 42); // 00042
// Alinhamento à esquerda com largura 10
println!("{:<10}!", "esquerda"); // esquerda !
// Alinhamento à direita com largura 10
println!("{:>10}!", "direita"); // direita!
// Centralizado com largura 10
println!("{:^10}!", "centro"); // centro !
// Precisão para ponto flutuante
println!("{:.2}", 3.14159); // 3.14
// Base numérica
println!("{:x}", 255); // ff
println!("{:b}", 5); // 101
// Argumentos posicionais
println!("{0} e {1} e {0}", "A", "B"); // A e B e A
}Além disso, é possível usar nomes em vez de posições, mas isso requer a sintaxe {nome} e passar argumentos nomeados, o que é feito com a macro format! usando a sintaxe nome = valor (disponível em Rust 1.58+).
fn main() {
let nome = "Bob";
let idade = 25;
println!("{nome} tem {idade} anos.", nome = nome, idade = idade);
}Implementando
Para tipos personalizados, implementar Display é essencial para fornecer uma saída legível. A implementação é feita definindo o método fmt do trait, que recebe um Formatter e retorna um fmt::Result. Dentro do método, você usa write! para escrever a representação.
Vamos implementar Display para uma struct Pessoa:
use std::fmt;
struct Pessoa {
nome: String,
idade: u8,
}
impl fmt::Display for Pessoa {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} ({} anos)", self.nome, self.idade)
}
}
fn main() {
let pessoa = Pessoa {
nome: String::from("Ana"),
idade: 28,
};
println!("{}", pessoa); // Ana (28 anos)
}Observe que usamos write! para escrever no formatter. O trait Display também permite usar especificadores de formatação, pois o Formatter fornece métodos como pad, width e precision que podem ser acessados via f.
Se você quiser que seu tipo também seja utilizável com {:?}, derive Debug ou implemente manualmente. Muitas vezes, é útil implementar ambos para ter flexibilidade.
Boas práticas e observações finais
Ao implementar Display, siga estas boas práticas: mantenha a saída concisa e legível, sem informações desnecessárias; não inclua detalhes de implementação; e considere usar Debug para informações mais detalhadas quando necessário.
Lembre-se de que Display é usado implicitamente em macros como println! quando você usa {}, então implementá-lo corretamente melhora a experiência de quem usa seu código.
Além disso, a formatação é uma área em constante evolução; consulte a documentação oficial para especificadores avançados e novas funcionalidades.
Referências
- Documentação oficial do módulo std::fmt
- Trait Display
- Trait Debug
- Macro format!
- Rust by Example: Formatação
- Referência: strings de formato
Exercícios
- Escreva um programa que use
format!para criar uma string com seu nome e idade, e depois imprima-a. - Crie uma struct
Pontocom campos x e y (f64) e implementeDisplaypara exibir como "(x, y)". - Use especificadores para imprimir um número com 3 casas decimais e preenchido com zeros à esquerda até largura 8.
- Explique a diferença entre
DisplayeDebuge dê um exemplo de quando usar cada um. - Implemente
Displaypara uma enumeraçãoStatuscom variantesAtivoeInativo.
fn main() {
let nome = "Carlos";
let idade = 30;
let msg = format!("Meu nome é {} e tenho {} anos.", nome, idade);
println!("{}", msg);
}use std::fmt;
struct Ponto {
x: f64,
y: f64,
}
impl fmt::Display for Ponto {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Ponto { x: 1.5, y: -2.0 };
println!("{}", p); // (1.5, -2)
}fn main() {
let numero = 3.14159;
println!("{:08.3}", numero); // 0003.142
}Display é para saída amigável ao usuário final, como em mensagens de interface. Debug é para depuração, mostrando representação interna. Exemplo: para um tipo Data, Display pode mostrar "25/12/2024", enquanto Debug pode mostrar "Data { dia: 25, mes: 12, ano: 2024 }"use std::fmt;
enum Status {
Ativo,
Inativo,
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Status::Ativo => write!(f, "ativo"),
Status::Inativo => write!(f, "inativo"),
}
}
}
fn main() {
let status = Status::Ativo;
println!("Status: {}", status); // Status: ativo
}