Display e Debug Traits em Rust

Guia completo sobre os traits Display e Debug em Rust: formatação de saída para usuários e desenvolvedores com exemplos práticos em português.

O que são Display e Debug?

Em Rust, os traits fmt::Display e fmt::Debug controlam como um tipo é convertido em texto. Eles são fundamentais para qualquer programa, pois determinam o que aparece quando você usa println!, format!, write! e outros macros de formatação.

  • Display ({}) produz saída voltada para o usuário final — limpa, legível e amigável.
  • Debug ({:?}) produz saída voltada para o desenvolvedor — detalhada, com nomes de campos e estrutura interna.

Praticamente todo tipo que você cria em Rust deveria implementar pelo menos Debug. Já Display é implementado quando o tipo tem uma representação textual natural que faz sentido para o usuário.


Definição dos Traits

fmt::Debug

// Definido em std::fmt
pub trait Debug {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
}

fmt::Display

// Definido em std::fmt
pub trait Display {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
}

Ambos têm exatamente a mesma assinatura — a diferença está no propósito e no marcador de formatação que os invoca.


Como Implementar: derive vs impl manual

Debug com #[derive]

A forma mais comum de implementar Debug é usando a macro derive. O compilador gera automaticamente uma implementação que mostra o nome do tipo e todos os campos:

#[derive(Debug)]
struct Produto {
    nome: String,
    preco: f64,
    em_estoque: bool,
}

fn main() {
    let p = Produto {
        nome: String::from("Teclado Mecânico"),
        preco: 299.90,
        em_estoque: true,
    };

    // Saída: Produto { nome: "Teclado Mecânico", preco: 299.9, em_estoque: true }
    println!("{:?}", p);

    // Formatação alternativa (pretty-print):
    // Produto {
    //     nome: "Teclado Mecânico",
    //     preco: 299.9,
    //     em_estoque: true,
    // }
    println!("{:#?}", p);
}

Para usar #[derive(Debug)], todos os campos do tipo também devem implementar Debug. Todos os tipos da biblioteca padrão já o fazem.

Debug com implementação manual

Quando você quer controlar exatamente o que aparece na saída de depuração:

use std::fmt;

struct SenhaProtegida {
    usuario: String,
    senha: String,
}

impl fmt::Debug for SenhaProtegida {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("SenhaProtegida")
            .field("usuario", &self.usuario)
            .field("senha", &"****")  // Nunca exibir a senha real
            .finish()
    }
}

fn main() {
    let cred = SenhaProtegida {
        usuario: String::from("admin"),
        senha: String::from("s3cr3t0"),
    };
    // Saída: SenhaProtegida { usuario: "admin", senha: "****" }
    println!("{:?}", cred);
}

Display com implementação manual

Display não pode ser derivado automaticamente — você sempre precisa implementá-lo manualmente. Isso faz sentido, pois a representação amigável depende do contexto do seu domínio:

use std::fmt;

struct Moeda {
    valor: f64,
    simbolo: &'static str,
}

impl fmt::Display for Moeda {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {:.2}", self.simbolo, self.valor)
    }
}

fn main() {
    let preco = Moeda { valor: 49.9, simbolo: "R$" };
    // Saída: R$ 49.90
    println!("{}", preco);

    // Display também é usado por .to_string()
    let texto: String = preco.to_string();
    assert_eq!(texto, "R$ 49.90");
}

Implementar Display automaticamente fornece .to_string() graças a uma implementação blanket na biblioteca padrão: impl<T: Display> ToString for T.


Exemplos Práticos

Exemplo 1: Enum com Display e Debug

use std::fmt;

#[derive(Debug)]
enum StatusPedido {
    Pendente,
    Processando,
    Enviado { codigo_rastreio: String },
    Entregue,
    Cancelado(String), // motivo
}

impl fmt::Display for StatusPedido {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            StatusPedido::Pendente => write!(f, "Pendente"),
            StatusPedido::Processando => write!(f, "Em processamento"),
            StatusPedido::Enviado { codigo_rastreio } => {
                write!(f, "Enviado (rastreio: {})", codigo_rastreio)
            }
            StatusPedido::Entregue => write!(f, "Entregue"),
            StatusPedido::Cancelado(motivo) => {
                write!(f, "Cancelado: {}", motivo)
            }
        }
    }
}

fn main() {
    let status = StatusPedido::Enviado {
        codigo_rastreio: String::from("BR123456789"),
    };

    // Display: Enviado (rastreio: BR123456789)
    println!("Status: {}", status);

    // Debug: Enviado { codigo_rastreio: "BR123456789" }
    println!("Debug: {:?}", status);
}

Exemplo 2: Usando f.write_str e o Formatter

use std::fmt;

struct Coordenada {
    lat: f64,
    lon: f64,
}

impl fmt::Display for Coordenada {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // f.write_str é mais eficiente que write! para strings fixas
        f.write_str("(")?;
        write!(f, "{:.4}, {:.4}", self.lat, self.lon)?;
        f.write_str(")")
    }
}

impl fmt::Debug for Coordenada {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Verifica se o modo alternativo {:#?} foi solicitado
        if f.alternate() {
            write!(
                f,
                "Coordenada {{\n  latitude: {},\n  longitude: {},\n}}",
                self.lat, self.lon
            )
        } else {
            write!(f, "Coordenada({}, {})", self.lat, self.lon)
        }
    }
}

fn main() {
    let local = Coordenada { lat: -23.5505, lon: -46.6333 };

    println!("{}", local);    // (-23.5505, -46.6333)
    println!("{:?}", local);  // Coordenada(-23.5505, -46.6333)
    println!("{:#?}", local);
    // Coordenada {
    //   latitude: -23.5505,
    //   longitude: -46.6333,
    // }
}

Exemplo 3: Debug para coleções e tipos aninhados

#[derive(Debug)]
struct Aluno {
    nome: String,
    notas: Vec<f64>,
}

#[derive(Debug)]
struct Turma {
    disciplina: String,
    alunos: Vec<Aluno>,
}

fn main() {
    let turma = Turma {
        disciplina: String::from("Programação Rust"),
        alunos: vec![
            Aluno {
                nome: String::from("Ana"),
                notas: vec![9.5, 8.0, 10.0],
            },
            Aluno {
                nome: String::from("Bruno"),
                notas: vec![7.5, 8.5, 9.0],
            },
        ],
    };

    // {:#?} formata a estrutura aninhada de forma legível
    println!("{:#?}", turma);
}

Exemplo 4: format! macro e formatação condicional

use std::fmt;

#[derive(Debug)]
struct Temperatura {
    celsius: f64,
}

impl fmt::Display for Temperatura {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Respeita a precisão solicitada pelo chamador
        if let Some(precisao) = f.precision() {
            write!(f, "{:.prec$}°C", self.celsius, prec = precisao)
        } else {
            write!(f, "{:.1}°C", self.celsius)
        }
    }
}

fn main() {
    let temp = Temperatura { celsius: 23.456 };

    // Usa a precisão padrão do Display (1 casa)
    println!("{}", temp);           // 23.5°C

    // Solicita 3 casas decimais
    println!("{:.3}", temp);        // 23.456°C

    // format! retorna uma String
    let msg = format!("A temperatura é {:.0}", temp);
    println!("{}", msg);            // A temperatura é 23°C
}

Exemplo 5: Display para tipos wrapper (newtype)

use std::fmt;

struct Cpf([u8; 11]);

impl Cpf {
    fn new(digitos: [u8; 11]) -> Self {
        Cpf(digitos)
    }
}

impl fmt::Display for Cpf {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let d = &self.0;
        write!(
            f,
            "{}{}{}.{}{}{}.{}{}{}-{}{}",
            d[0], d[1], d[2], d[3], d[4], d[5],
            d[6], d[7], d[8], d[9], d[10]
        )
    }
}

impl fmt::Debug for Cpf {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Cpf(\"{}\")", self)  // Reutiliza Display
    }
}

fn main() {
    let cpf = Cpf::new([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1]);
    println!("{}", cpf);   // 123.456.789-01
    println!("{:?}", cpf); // Cpf("123.456.789-01")
}

Padrões e Boas Práticas

  1. Sempre derive Debug: Adicione #[derive(Debug)] a todos os seus tipos. Isso facilita depuração, logs e mensagens de erro.

  2. Implemente Display quando faz sentido: Nem todo tipo precisa de Display. Implemente-o quando seu tipo tem uma representação textual natural (como moeda, data, CPF, coordenadas).

  3. Oculte dados sensíveis: Em implementações manuais de Debug, nunca exponha senhas, tokens ou dados pessoais. Use "****" ou "[REDACTED]".

  4. Reutilize Display em Debug: Se seu tipo tem um Display bom, sua implementação de Debug pode referenciá-lo com write!(f, "NomeTipo(\"{}\")", self).

  5. Use {:#?} para depuração: A formatação alternativa (#) produz saída indentada e multilinha, muito mais legível para estruturas complexas.

  6. Display é necessário para Error: O trait std::error::Error exige que o tipo implemente Display. Veja Error Trait para mais detalhes.

  7. Prefira write! a concatenação: Dentro de fmt, use write!(f, ...) em vez de construir strings intermediárias. Isso evita alocações desnecessárias.


Veja Também