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
.0ou.valor(), considereDeref - 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.