Formatação e o trait Display
Esta aula explora a formatação de strings em Rust, focando no trait Display e na macro format!. Você aprenderá a diferença entre Display e Debug, como usar especificadores de formatação e como implementar Display para seus próprios tipos.
Em Rust, a formatação de strings é uma tarefa comum e poderosa, essencial para gerar saídas legíveis para o usuário ou para depuração. A linguagem fornece um sistema de formatação baseado em traits, principalmente Display e Debug, que controlam como os valores são convertidos em texto. Nesta aula, vamos mergulhar na macro format!, entender as diferenças entre Display e Debug, explorar os especificadores de formatação e aprender a implementar o trait Display para tipos personalizados.
Dominar a formatação é crucial para criar interfaces de linha de comando claras, gerar relatórios, ou simplesmente para registrar informações de forma eficaz. Vamos começar com a base: a macro format!.
format!
A macro format! é a maneira mais direta de criar uma string formatada em Rust. Ela recebe uma string de formato e argumentos, e retorna uma String contendo o resultado. É semelhante ao printf de C, mas com segurança de tipos e suporte a traits.
O primeiro argumento é a string de formato, que contém placeholders como {} (para Display) e {:?} (para Debug). Os argumentos seguintes são os valores que serão inseridos nos placeholders, na ordem em que aparecem. É possível usar índices para referenciar argumentos específicos, como {0} e {1}, e até mesmo nomes (se você usar format!("{nome}", nome = "valor")).
Exemplo básico:
let nome = "Maria";
let idade = 30;
let mensagem = format!("Olá, {}! Você tem {} anos.", nome, idade);
println!("{}", mensagem);
// Saída: Olá, Maria! Você tem 30 anos.
Além de format!, existem outras macros que usam o mesmo sistema de formatação: println!, print!, eprintln!, eprint!, write! e writeln!. Todas elas compartilham a mesma sintaxe de formato, mas diferem no destino da saída: println! imprime no stdout com uma nova linha, eprintln! no stderr, e write! escreve em um implementador de Write, como um arquivo ou um buffer.
Vamos ver um exemplo com write!:
use std::fmt::Write;
let mut buffer = String::new();
write!(&mut buffer, "Valor: {}", 42).unwrap();
println!("{}", buffer); // Saída: Valor: 42
Display vs Debug
Rust possui dois traits principais para formatação: Display e Debug. Ambos são usados para converter um valor em uma string, mas com propósitos diferentes.
Display (representado por {} na string de formato) é destinado à apresentação para o usuário final. Deve ser uma representação legível e amigável, como "42" para um número ou "Maria" para uma pessoa. O trait Display não é implementado automaticamente para a maioria dos tipos; você precisa implementá-lo manualmente se quiser usar {} com seus próprios tipos.
Por outro lado, Debug (representado por {:?}) é destinado à depuração e logging. Ele deve fornecer uma representação mais detalhada e não ambígua, frequentemente incluindo estruturas internas. O trait Debug pode ser derivado automaticamente usando #[derive(Debug)] para a maioria dos tipos, o que é extremamente útil.
Exemplo:
#[derive(Debug)]
struct Ponto {
x: i32,
y: i32,
}
let p = Ponto { x: 10, y: 20 };
println!("Debug: {:?}", p); // Saída: Debug: Ponto { x: 10, y: 20 }
// println!("Display: {}", p); // Erro: Ponto não implementa Display
Observe que tentar usar {} com Ponto causa um erro de compilação, pois Display não foi implementado. Isso é uma garantia de que você não exibirá acidentalmente uma representação inadequada para o usuário.
Especificadores
Os especificadores de formatação permitem controlar como os valores são exibidos dentro dos placeholders. Eles são escritos após o : dentro das chaves, por exemplo {:?} para Debug, {:x} para hexadecimal, etc. Vamos explorar os principais especificadores.
Para números, você pode especificar a base: {:b} para binário, {:o} para octal, {:x} para hexadecimal minúsculo, e {:X} para hexadecimal maiúsculo. Por exemplo:
let n = 255;
println!("Dec: {}", n); // Dec: 255
println!("Hex: {:x}", n); // Hex: ff
println!("Hex: {:X}", n); // Hex: FF
println!("Oct: {:o}", n); // Oct: 377
println!("Bin: {:b}", n); // Bin: 11111111
Você pode controlar a largura mínima do campo usando {:width}. Por exemplo, {:5} garante que a string tenha pelo menos 5 caracteres, preenchendo com espaços à esquerda (por padrão). Para alinhar à esquerda, use {:<5}, e para centralizar, {:^5}. Você também pode preencher com um caractere específico, como {:0>5} para preencher com zeros à direita.
Exemplo:
println!("|{:5}|", 42); // | 42|
println!("|{:<5}|", 42); // |42 |
println!("|{:^5}|", 42); // | 42 |
println!("|{:0>5}|", 42); // |00042|
Para números de ponto flutuante, você pode controlar a precisão com {:.precisao}. Por exemplo, {:.2} exibe duas casas decimais. Isso também funciona com strings para truncar o número de caracteres.
let pi = 3.14159;
println!("{:.2}", pi); // 3.14
println!("{:.4}", pi); // 3.1416
Além disso, existe o especificador {:?} para Debug, e {:p} para ponteiros (endereços de memória). Você pode combinar especificadores, como {:5.2} para largura e precisão.
É importante notar que a formatação é verificada em tempo de compilação: se você tentar usar um especificador incompatível com o tipo, o compilador emitirá um erro. Isso é uma grande vantagem em relação a linguagens como C.
Implementando
Para usar {} com seus próprios tipos, você deve implementar o trait Display. A implementação requer um método fmt que recebe um Formatter e retorna um fmt::Result. Dentro do método, você usa a macro write! para escrever a representação desejada.
Vamos implementar Display para o struct Ponto que definimos anteriormente:
use std::fmt;
struct Ponto {
x: i32,
y: i32,
}
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: 10, y: 20 };
println!("{}", p); // Saída: (10, 20)
}
O método fmt recebe um Formatter que fornece acesso aos especificadores de formatação (como largura e precisão). Você pode usar os métodos do formatter, como f.pad(), para respeitar a largura solicitada. Por exemplo, se o usuário escrever "{:>10}", seu Display deve preencher com espaços à esquerda até atingir 10 caracteres. Para lidar com isso, você pode usar f.pad(&self.string_representation).
Vamos melhorar a implementação para respeitar a largura:
impl fmt::Display for Ponto {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = format!("({}, {})", self.x, self.y);
f.pad(&s)
}
}
Agora, println!("{:>15}", p) alinhará à direita em um campo de 15 caracteres.
É importante também considerar a implementação de Display para tipos que contêm outros tipos. Você pode delegar a formatação dos campos usando write! com os placeholders apropriados. Lembre-se de que Display deve ser uma representação não ambígua e legível para o usuário final; para depuração, use Debug derivado.
Boas práticas
Ao implementar Display, siga estas boas práticas:
- Sempre implemente
Displaypara tipos que serão exibidos ao usuário em logs, mensagens de erro ou interfaces. - Use
Debugpara representações de depuração e derive-o automaticamente sempre que possível. - Respeite os especificadores de formatação, especialmente largura e precisão, usando
f.pad()ef.precision(). - Evite alocações desnecessárias dentro de
fmt; prefira escrever diretamente no formatter comwrite!. - Teste a formatação com diferentes especificadores para garantir que seu código se comporta como esperado.
Além disso, lembre-se de que a macro format! é avaliada em tempo de compilação, então erros de tipo ou de especificador são capturados cedo.
Referências
- Documentação oficial de std::fmt
- Rust by Example: Display
- Rust by Example: Debug
- Trait Display
- Trait Debug
- Macro format!
Exercícios
- Escreva um programa que use
format!para criar uma string que combine um nome e uma idade, e depois imprima-a usandoprintln!. - Dado o struct
Livrocom campostitulo(String) eautor(String), implementeDisplaypara que ele seja exibido como "Título por Autor". Derive tambémDebug. - Use especificadores de formatação para imprimir o número 255 em binário, octal, hexadecimal (minúsculo e maiúsculo), e com largura mínima de 10 caracteres preenchido com zeros.
- Escreva um programa que leia um número de ponto flutuante do usuário e imprima com exatamente duas casas decimais, usando
format!ouprintln!. - Implemente
Displaypara um enumCorque pode serVermelho,VerdeouAzul, e useformat!para criar uma string com a cor e um texto.
fn main() {
let nome = "João";
let idade = 25;
let mensagem = format!("Nome: {}, Idade: {}", nome, idade);
println!("{}", mensagem);
}
use std::fmt;
#[derive(Debug)]
struct Livro {
titulo: String,
autor: String,
}
impl fmt::Display for Livro {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} por {}", self.titulo, self.autor)
}
}
fn main() {
let livro = Livro { titulo: "O Senhor dos Anéis".to_string(), autor: "J.R.R. Tolkien".to_string() };
println!("{}", livro);
println!("{:?}", livro);
}
fn main() {
let n = 255;
println!("Binário: {:b}", n);
println!("Octal: {:o}", n);
println!("Hex minúsculo: {:x}", n);
println!("Hex maiúsculo: {:X}", n);
println!("Largura 10 com zeros: {:010}", n);
}
use std::io;
fn main() {
let mut input = String::new();
println!("Digite um número decimal:");
io::stdin().read_line(&mut input).expect("Falha ao ler linha");
let numero: f64 = input.trim().parse().expect("Número inválido");
println!("Com duas casas decimais: {:.2}", numero);
}
use std::fmt;
enum Cor {
Vermelho,
Verde,
Azul,
}
impl fmt::Display for Cor {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Cor::Vermelho => "vermelho",
Cor::Verde => "verde",
Cor::Azul => "azul",
};
write!(f, "{}", s)
}
}
fn main() {
let cor = Cor::Verde;
let mensagem = format!("A cor escolhida é {}.", cor);
println!("{}", mensagem);
}
Observações finais
Nesta aula, você aprendeu os fundamentos da formatação em Rust. Dominar format!, Display e Debug é essencial para criar programas claros e profissionais. Lembre-se de que a implementação de Display é uma forma de comunicação com o usuário, então invista tempo para torná-la legível e útil. Continue praticando com diferentes tipos e especificadores para solidificar seu conhecimento.