Newtype Pattern em Rust: Segurança de Tipos com Custo Zero

Guia completo do padrão Newtype em Rust: tuple structs como wrappers de tipo, segurança semântica, Deref, implementação de traits em tipos estrangeiros e abstrações de custo zero.

O Newtype Pattern é um dos padrões mais idiomáticos e poderosos do Rust. Ele consiste em envolver um tipo existente em uma tuple struct de um único campo, criando um tipo completamente novo com semântica própria. O resultado é uma abstração de custo zero: em tempo de execução, o newtype tem exatamente a mesma representação em memória que o tipo interno.

Este padrão resolve problemas que vão desde segurança de tipos (impedir que metros sejam confundidos com quilômetros) até contornar a regra de orfandade do Rust (implementar traits estrangeiras em tipos estrangeiros). É o canivete suíço do programador Rust.

Problema

Considere uma função que calcula a velocidade a partir de distância e tempo:

// PERIGOSO: ambos os parametros sao f64!
fn calcular_velocidade(distancia: f64, tempo: f64) -> f64 {
    distancia / tempo
}

fn main() {
    let distancia_km = 150.0;
    let tempo_horas = 2.0;

    // Acidentalmente trocamos os argumentos — compila sem erro!
    let velocidade = calcular_velocidade(tempo_horas, distancia_km);
    // Resultado: 0.0133... em vez de 75.0 — bug silencioso!
}

O compilador nao consegue distinguir “metros” de “segundos” porque ambos sao f64. Aliases de tipo (type Metros = f64) tambem nao ajudam, pois sao apenas sinonimos — Metros e f64 sao intercambiaveis.

Solução em Rust

Newtype Basico

/// Distancia em quilometros — tipo distinto de f64
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Quilometros(f64);

/// Tempo em horas — tipo distinto de f64
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Horas(f64);

/// Velocidade em km/h — tipo distinto de f64
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct KmPorHora(f64);

impl Quilometros {
    fn novo(valor: f64) -> Self {
        assert!(valor >= 0.0, "Distância não pode ser negativa");
        Quilometros(valor)
    }

    fn valor(&self) -> f64 {
        self.0
    }
}

impl Horas {
    fn nova(valor: f64) -> Self {
        assert!(valor > 0.0, "Tempo deve ser positivo");
        Horas(valor)
    }

    fn valor(&self) -> f64 {
        self.0
    }
}

impl KmPorHora {
    fn valor(&self) -> f64 {
        self.0
    }
}

/// Agora a assinatura torna impossível trocar argumentos
fn calcular_velocidade(distancia: Quilometros, tempo: Horas) -> KmPorHora {
    KmPorHora(distancia.valor() / tempo.valor())
}

fn main() {
    let dist = Quilometros::novo(150.0);
    let tempo = Horas::nova(2.0);

    let vel = calcular_velocidade(dist, tempo);
    println!("Velocidade: {:.1} km/h", vel.valor()); // 75.0 km/h

    // ERRO DE COMPILAÇÃO: tipos incompatíveis!
    // let vel = calcular_velocidade(tempo, dist);
    //                                ^^^^^ esperado Quilometros, encontrado Horas
}

Implementando Operacoes Matematicas

use std::ops::{Add, Sub, Mul, Div};

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Metros(f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Segundos(f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct MetrosPorSegundo(f64);

/// Metros + Metros = Metros (faz sentido)
impl Add for Metros {
    type Output = Metros;
    fn add(self, outro: Metros) -> Metros {
        Metros(self.0 + outro.0)
    }
}

/// Metros - Metros = Metros
impl Sub for Metros {
    type Output = Metros;
    fn sub(self, outro: Metros) -> Metros {
        Metros(self.0 - outro.0)
    }
}

/// Metros / Segundos = MetrosPorSegundo (análise dimensional!)
impl Div<Segundos> for Metros {
    type Output = MetrosPorSegundo;
    fn div(self, tempo: Segundos) -> MetrosPorSegundo {
        MetrosPorSegundo(self.0 / tempo.0)
    }
}

/// Metros * escalar = Metros
impl Mul<f64> for Metros {
    type Output = Metros;
    fn mul(self, escalar: f64) -> Metros {
        Metros(self.0 * escalar)
    }
}

impl std::fmt::Display for Metros {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:.2} m", self.0)
    }
}

impl std::fmt::Display for MetrosPorSegundo {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:.2} m/s", self.0)
    }
}

fn main() {
    let distancia = Metros(100.0);
    let tempo = Segundos(9.58); // Recorde de Usain Bolt

    let velocidade = distancia / tempo;
    println!("Velocidade: {}", velocidade); // 10.44 m/s

    let total = Metros(100.0) + Metros(200.0);
    println!("Distância total: {}", total); // 300.00 m

    // ERRO: Metros + Segundos não faz sentido — não compila!
    // let absurdo = Metros(5.0) + Segundos(3.0);
}

Usando Deref para Acesso Transparente

use std::ops::Deref;

/// Email validado — garante formato correto na criação
#[derive(Debug, Clone, PartialEq)]
struct Email(String);

#[derive(Debug)]
enum ErroEmail {
    FormatoInvalido(String),
    DominioInvalido(String),
}

impl std::fmt::Display for ErroEmail {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ErroEmail::FormatoInvalido(e) => write!(f, "Formato inválido: {}", e),
            ErroEmail::DominioInvalido(d) => write!(f, "Domínio inválido: {}", d),
        }
    }
}

impl Email {
    /// Cria um Email validado — única forma de construção
    fn novo(valor: &str) -> Result<Self, ErroEmail> {
        // Validação básica de formato
        if !valor.contains('@') {
            return Err(ErroEmail::FormatoInvalido(valor.to_string()));
        }

        let partes: Vec<&str> = valor.split('@').collect();
        if partes.len() != 2 || partes[0].is_empty() || partes[1].is_empty() {
            return Err(ErroEmail::FormatoInvalido(valor.to_string()));
        }

        // Verificar se o domínio tem pelo menos um ponto
        if !partes[1].contains('.') {
            return Err(ErroEmail::DominioInvalido(partes[1].to_string()));
        }

        Ok(Email(valor.to_lowercase()))
    }

    /// Retorna o domínio do email
    fn dominio(&self) -> &str {
        self.0.split('@').nth(1).unwrap()
    }

    /// Retorna o usuário (parte antes do @)
    fn usuario(&self) -> &str {
        self.0.split('@').next().unwrap()
    }
}

/// Deref permite usar Email onde &str é esperado
impl Deref for Email {
    type Target = str;
    fn deref(&self) -> &str {
        &self.0
    }
}

impl std::fmt::Display for Email {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

fn enviar_email(destinatario: &Email, assunto: &str) {
    println!("Enviando '{}' para {}", assunto, destinatario);
    // Graças a Deref, podemos usar métodos de &str
    println!("  Tamanho do email: {} caracteres", destinatario.len());
    println!("  Domínio: {}", destinatario.dominio());
}

fn main() {
    // Criação validada
    let email = Email::novo("usuario@exemplo.com.br").unwrap();
    enviar_email(&email, "Bem-vindo ao Rust!");

    // Tentativas inválidas
    match Email::novo("sem-arroba") {
        Err(e) => println!("Erro esperado: {}", e),
        Ok(_) => unreachable!(),
    }

    match Email::novo("user@semdominio") {
        Err(e) => println!("Erro esperado: {}", e),
        Ok(_) => unreachable!(),
    }
}

Implementando Traits Estrangeiras em Tipos Estrangeiros

/// Regra de orfandade: não podemos implementar Display para Vec<T>
/// Mas podemos com newtype!
struct ListaFormatada<T>(Vec<T>);

impl<T: std::fmt::Display> std::fmt::Display for ListaFormatada<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let itens: Vec<String> = self.0.iter().map(|i| i.to_string()).collect();
        write!(f, "[{}]", itens.join(", "))
    }
}

fn main() {
    let lista = ListaFormatada(vec![1, 2, 3, 4, 5]);
    println!("Lista: {}", lista); // Lista: [1, 2, 3, 4, 5]

    let nomes = ListaFormatada(vec!["Ana", "Bruno", "Carla"]);
    println!("Nomes: {}", nomes); // Nomes: [Ana, Bruno, Carla]
}

Diagrama

    Representação em memória — Custo Zero:

    f64 puro:         ┌──────────────────┐
                      │   150.0 (8 bytes) │
                      └──────────────────┘

    Quilometros(f64): ┌──────────────────┐
                      │   150.0 (8 bytes) │  ← MESMA representação!
                      └──────────────────┘

    O compilador otimiza o wrapper — sem overhead em runtime.

    Segurança de tipos em compilação:

    ┌────────────┐    ┌────────────┐    ┌───────────────┐
    │ Metros(f64)│    │Segundos(f64)│   │ MetrosPorSeg  │
    └─────┬──────┘    └─────┬──────┘    │   (f64)       │
          │                 │           └───────────────┘
          │    Div<Segundos>│                   ▲
          └────────/────────┘                   │
                   │                            │
                   └────── resultado ───────────┘

    Regra de orfandade contornada:

    Crate externo          Seu crate
    ┌──────────┐          ┌─────────────────┐
    │ Vec<T>   │          │ ListaFormatada  │
    │          │    wrap   │ (Vec<T>)        │
    │ Display? │◄─ NÃO ──│ Display? SIM!   │
    └──────────┘          └─────────────────┘

Exemplo do Mundo Real

Um sistema de unidades de medida completo para engenharia:

use std::ops::{Add, Sub, Mul, Div};

/// Macro para gerar newtypes numéricos com operações padrão
macro_rules! newtype_numerico {
    ($nome:ident, $unidade:expr) => {
        #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
        struct $nome(f64);

        impl $nome {
            fn novo(valor: f64) -> Self {
                $nome(valor)
            }

            fn valor(&self) -> f64 {
                self.0
            }
        }

        impl Add for $nome {
            type Output = $nome;
            fn add(self, outro: $nome) -> $nome {
                $nome(self.0 + outro.0)
            }
        }

        impl Sub for $nome {
            type Output = $nome;
            fn sub(self, outro: $nome) -> $nome {
                $nome(self.0 - outro.0)
            }
        }

        impl Mul<f64> for $nome {
            type Output = $nome;
            fn mul(self, escalar: f64) -> $nome {
                $nome(self.0 * escalar)
            }
        }

        impl std::fmt::Display for $nome {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, "{:.2} {}", self.0, $unidade)
            }
        }
    };
}

// Gerando tipos de unidade automaticamente
newtype_numerico!(Newtons, "N");
newtype_numerico!(Quilogramas, "kg");
newtype_numerico!(MetrosSegundo2, "m/s²");
newtype_numerico!(Pascals, "Pa");
newtype_numerico!(MetrosQuadrados, "m²");

/// Força = Massa × Aceleração (F = m·a)
impl Mul<MetrosSegundo2> for Quilogramas {
    type Output = Newtons;
    fn mul(self, aceleracao: MetrosSegundo2) -> Newtons {
        Newtons(self.0 * aceleracao.0)
    }
}

/// Pressão = Força / Área (P = F/A)
impl Div<MetrosQuadrados> for Newtons {
    type Output = Pascals;
    fn div(self, area: MetrosQuadrados) -> Pascals {
        Pascals(self.0 / area.0)
    }
}

/// ID de usuário — newtype para identificadores
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UserId(u64);

/// ID de pedido — tipo diferente de UserId, mesmo tipo interno
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct PedidoId(u64);

/// Tentativa de usar PedidoId onde UserId é esperado: ERRO!
fn buscar_usuario(id: UserId) -> String {
    format!("Usuário #{}", id.0)
}

fn main() {
    // Sistema de unidades com análise dimensional
    let massa = Quilogramas::novo(70.0);
    let aceleracao = MetrosSegundo2::novo(9.81);

    let peso = massa * aceleracao; // F = m·a
    println!("Peso: {}", peso); // 686.70 N

    let area = MetrosQuadrados::novo(0.05);
    let pressao = peso / area; // P = F/A
    println!("Pressão: {}", pressao); // 13734.00 Pa

    // IDs seguros — não podem ser confundidos
    let user = UserId(42);
    let pedido = PedidoId(42); // Mesmo valor, tipo diferente

    println!("{}", buscar_usuario(user));

    // ERRO DE COMPILAÇÃO:
    // println!("{}", buscar_usuario(pedido));
    //                               ^^^^^^ esperado UserId, encontrado PedidoId
}

Quando Usar

  • Segurança semântica: Quando dois valores do mesmo tipo primitivo representam conceitos diferentes
  • Unidades de medida: Metros, segundos, quilogramas — evitar conversões acidentais
  • Identificadores tipados: UserId vs PedidoId vs ProdutoId
  • Validação na criação: Email, CPF, CEP — tipos que garantem formato válido
  • Contornar regra de orfandade: Implementar traits externas em tipos externos
  • APIs seguras: Impossibilitar uso incorreto de parâmetros em funções

Quando NÃO Usar

  • Tipo já é suficientemente descritivo: Se a função recebe (nome: &str, email: &str) e o contexto é claro
  • Ergonomia excessivamente prejudicada: Se o wrapper torna o código 5x mais verboso sem benefício real
  • Tipo interno precisa ser exposto frequentemente: Se todo uso requer .0 ou .valor(), considere Deref
  • Prototipos rápidos: Em fases iniciais, newtypes podem atrapalhar a velocidade de desenvolvimento

Variações em Rust

Newtype com serde para serialização

use std::fmt;

/// CEP validado — serializa como string
#[derive(Debug, Clone, PartialEq)]
struct CEP(String);

impl CEP {
    fn novo(valor: &str) -> Result<Self, String> {
        let limpo: String = valor.chars().filter(|c| c.is_ascii_digit()).collect();
        if limpo.len() != 8 {
            return Err(format!("CEP deve ter 8 dígitos, encontrado: {}", limpo.len()));
        }
        Ok(CEP(format!("{}-{}", &limpo[..5], &limpo[5..])))
    }

    fn formatado(&self) -> &str {
        &self.0
    }
}

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

fn main() {
    let cep = CEP::novo("01310-100").unwrap();
    println!("CEP: {}", cep); // 01310-100

    let cep2 = CEP::novo("01310100").unwrap();
    println!("CEP: {}", cep2); // 01310-100

    match CEP::novo("123") {
        Err(e) => println!("Erro: {}", e),
        Ok(_) => unreachable!(),
    }
}

Newtype genérico com PhantomData

use std::marker::PhantomData;

/// Marcadores de unidade (zero-size)
struct Reais;
struct Dolares;
struct Euros;

/// Valor monetário parametrizado pela moeda
#[derive(Debug, Clone, Copy)]
struct Dinheiro<Moeda> {
    centavos: i64,
    _moeda: PhantomData<Moeda>,
}

impl<M> Dinheiro<M> {
    fn novo(centavos: i64) -> Self {
        Dinheiro {
            centavos,
            _moeda: PhantomData,
        }
    }

    fn de_unidades(unidades: f64) -> Self {
        Dinheiro {
            centavos: (unidades * 100.0) as i64,
            _moeda: PhantomData,
        }
    }
}

/// Soma só funciona com mesma moeda!
impl<M> Add for Dinheiro<M> {
    type Output = Dinheiro<M>;
    fn add(self, outro: Dinheiro<M>) -> Dinheiro<M> {
        Dinheiro::novo(self.centavos + outro.centavos)
    }
}

fn main() {
    let preco = Dinheiro::<Reais>::de_unidades(49.90);
    let frete = Dinheiro::<Reais>::de_unidades(15.00);
    let total = preco + frete; // OK: ambos em Reais
    println!("Total: {} centavos", total.centavos);

    let dolares = Dinheiro::<Dolares>::de_unidades(10.0);
    // ERRO: não pode somar Reais com Dólares!
    // let absurdo = preco + dolares;
}

Padrões Relacionados

  • Type-State: Usa newtypes (ou ZSTs) como marcadores de estado em parâmetros genéricos
  • Builder: Builders frequentemente produzem newtypes validados
  • Adapter: Newtype é a forma mais simples de adapter em Rust
  • RAII: Newtypes com Drop implementam gerenciamento de recursos

Conclusão

O Newtype Pattern é uma das ferramentas mais fundamentais do Rust idiomático. Ele transforma o sistema de tipos em um aliado ativo na prevenção de bugs, sem custo em runtime. A combinação de tuple structs, Deref, implementação de traits e macros para geração de código torna possível criar sistemas de tipos ricos e expressivos que capturam invariantes do domínio diretamente no código. Quando bem aplicado, o newtype faz com que estados inválidos sejam irrepresentáveis e erros de lógica se tornem erros de compilação.