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 Display para tipos que serão exibidos ao usuário em logs, mensagens de erro ou interfaces.
  • Use Debug para 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() e f.precision().
  • Evite alocações desnecessárias dentro de fmt; prefira escrever diretamente no formatter com write!.
  • 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

Exercícios

  1. Escreva um programa que use format! para criar uma string que combine um nome e uma idade, e depois imprima-a usando println!.
  2. ✓ Resposta:
    fn main() {
        let nome = "João";
        let idade = 25;
        let mensagem = format!("Nome: {}, Idade: {}", nome, idade);
        println!("{}", mensagem);
    }
    
  3. Dado o struct Livro com campos titulo (String) e autor (String), implemente Display para que ele seja exibido como "Título por Autor". Derive também Debug.
  4. ✓ Resposta:
    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);
    }
    
  5. 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.
  6. ✓ Resposta:
    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);
    }
    
  7. Escreva um programa que leia um número de ponto flutuante do usuário e imprima com exatamente duas casas decimais, usando format! ou println!.
  8. ✓ Resposta:
    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);
    }
    
  9. Implemente Display para um enum Cor que pode ser Vermelho, Verde ou Azul, e use format! para criar uma string com a cor e um texto.
  10. ✓ Resposta:
    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.