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

Exercícios

  1. Escreva um programa que use format! para criar uma string com seu nome e idade, e depois imprima-a.
  2. ✓ Resposta:
    fn main() {
        let nome = "Carlos";
        let idade = 30;
        let msg = format!("Meu nome é {} e tenho {} anos.", nome, idade);
        println!("{}", msg);
    }
  3. Crie uma struct Ponto com campos x e y (f64) e implemente Display para exibir como "(x, y)".
  4. ✓ Resposta:
    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)
    }
  5. Use especificadores para imprimir um número com 3 casas decimais e preenchido com zeros à esquerda até largura 8.
  6. ✓ Resposta:
    fn main() {
        let numero = 3.14159;
        println!("{:08.3}", numero); // 0003.142
    }
  7. Explique a diferença entre Display e Debug e dê um exemplo de quando usar cada um.
  8. ✓ Resposta: 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 }"
  9. Implemente Display para uma enumeração Status com variantes Ativo e Inativo.
  10. ✓ Resposta:
    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
    }