Structs e enums são as ferramentas fundamentais para modelar dados em Rust. Combinados com pattern matching, eles permitem escrever código expressivo, seguro e elegante. Neste tutorial, vamos explorar cada um desses conceitos em profundidade.
Structs (Estruturas)
Structs permitem agrupar dados relacionados em um único tipo. Existem três tipos de structs em Rust.
Struct Regular (Named Fields)
A forma mais comum, com campos nomeados:
struct Usuario {
nome: String,
email: String,
idade: u32,
ativo: bool,
}
fn main() {
// Criando uma instância
let usuario = Usuario {
nome: String::from("Maria Silva"),
email: String::from("maria@exemplo.com"),
idade: 28,
ativo: true,
};
println!("Nome: {}", usuario.nome);
println!("Email: {}", usuario.email);
println!("Idade: {}", usuario.idade);
println!("Ativo: {}", usuario.ativo);
}
Struct Mutável
Para modificar campos, a variável inteira deve ser mut (Rust não permite mutabilidade por campo):
struct Usuario {
nome: String,
email: String,
idade: u32,
ativo: bool,
}
fn main() {
let mut usuario = Usuario {
nome: String::from("Carlos"),
email: String::from("carlos@exemplo.com"),
idade: 30,
ativo: true,
};
usuario.email = String::from("carlos.novo@exemplo.com");
usuario.idade = 31;
println!("{} - {}", usuario.nome, usuario.email);
}
Field Init Shorthand e Struct Update Syntax
struct Usuario {
nome: String,
email: String,
idade: u32,
ativo: bool,
}
fn criar_usuario(nome: String, email: String) -> Usuario {
Usuario {
nome, // shorthand: 'nome: nome' vira apenas 'nome'
email, // shorthand: 'email: email' vira apenas 'email'
idade: 0,
ativo: true,
}
}
fn main() {
let usuario1 = criar_usuario(
String::from("Ana"),
String::from("ana@exemplo.com"),
);
// Struct update syntax: cria um novo a partir de um existente
let usuario2 = Usuario {
nome: String::from("Bruno"),
email: String::from("bruno@exemplo.com"),
..usuario1 // usa os campos restantes de usuario1
};
// Cuidado: usuario1.nome e usuario1.email foram movidos para usuario2!
// Mas usuario1.idade e usuario1.ativo foram copiados (implementam Copy)
println!("{} - idade: {}", usuario2.nome, usuario2.idade);
}
Tuple Structs
Structs sem nomes nos campos, úteis para criar tipos distintos a partir de tuplas:
struct Cor(u8, u8, u8);
struct Ponto(f64, f64);
struct Metros(f64);
struct Quilometros(f64);
fn main() {
let vermelho = Cor(255, 0, 0);
let origem = Ponto(0.0, 0.0);
let distancia = Metros(100.0);
println!("R: {}, G: {}, B: {}", vermelho.0, vermelho.1, vermelho.2);
println!("Ponto: ({}, {})", origem.0, origem.1);
println!("Distância: {}m", distancia.0);
// Tipo seguro: não pode misturar Metros com Quilometros
let _km = Quilometros(5.0);
// let soma = distancia.0 + _km.0; // Funciona, mas semanticamente errado
// Structs diferentes = tipos diferentes no compilador
}
Unit Structs
Structs sem campos, úteis para implementar traits:
struct Vazio;
fn main() {
let _v = Vazio;
// Unit structs são úteis quando você precisa implementar
// um trait mas não tem dados para armazenar
}
Blocos impl: Métodos e Funções Associadas
O bloco impl permite adicionar métodos e funções associadas a structs:
struct Retangulo {
largura: f64,
altura: f64,
}
impl Retangulo {
// Função associada (como "static method" em outras linguagens)
// Não recebe &self — chamada com Retangulo::novo()
fn novo(largura: f64, altura: f64) -> Self {
Self { largura, altura }
}
fn quadrado(lado: f64) -> Self {
Self {
largura: lado,
altura: lado,
}
}
// Método: recebe &self (referência imutável)
fn area(&self) -> f64 {
self.largura * self.altura
}
fn perimetro(&self) -> f64 {
2.0 * (self.largura + self.altura)
}
fn eh_quadrado(&self) -> bool {
(self.largura - self.altura).abs() < f64::EPSILON
}
// Método que recebe &mut self (pode modificar)
fn redimensionar(&mut self, fator: f64) {
self.largura *= fator;
self.altura *= fator;
}
// Método que consome self (toma ownership)
fn dividir(self) -> (Retangulo, Retangulo) {
let metade_largura = self.largura / 2.0;
(
Retangulo::novo(metade_largura, self.altura),
Retangulo::novo(metade_largura, self.altura),
)
}
}
fn main() {
// Usando funções associadas
let mut ret = Retangulo::novo(10.0, 5.0);
let quad = Retangulo::quadrado(7.0);
println!("Retângulo: {}x{}", ret.largura, ret.altura);
println!("Área: {}", ret.area());
println!("Perímetro: {}", ret.perimetro());
println!("É quadrado? {}", ret.eh_quadrado());
println!("\nQuadrado: {}x{}", quad.largura, quad.altura);
println!("É quadrado? {}", quad.eh_quadrado());
// Método mutável
ret.redimensionar(2.0);
println!("\nApós redimensionar: {}x{}", ret.largura, ret.altura);
println!("Nova área: {}", ret.area());
// Método que consome ownership
let (esq, dir) = ret.dividir();
// ret não é mais válido aqui!
println!("\nDividido: {}x{} e {}x{}", esq.largura, esq.altura, dir.largura, dir.altura);
}
Os três tipos de self:
&self— Empresta a struct (leitura apenas, mais comum)&mut self— Empresta mutavelmente (pode modificar)self— Toma ownership (consome a struct)
Exibindo Structs com Debug e Display
// Derive Debug para impressão de depuração
#[derive(Debug)]
struct Produto {
nome: String,
preco: f64,
estoque: u32,
}
impl std::fmt::Display for Produto {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} - R$ {:.2} ({} em estoque)", self.nome, self.preco, self.estoque)
}
}
fn main() {
let produto = Produto {
nome: String::from("Teclado Mecânico"),
preco: 299.90,
estoque: 15,
};
println!("Debug: {:?}", produto); // usa Debug
println!("Display: {}", produto); // usa Display
println!("Pretty: {:#?}", produto); // Debug formatado
}
Enums (Enumerações)
Enums representam tipos que podem ser uma entre várias variantes. Em Rust, cada variante pode carregar dados:
Enum Simples
enum DiaDaSemana {
Segunda,
Terca,
Quarta,
Quinta,
Sexta,
Sabado,
Domingo,
}
fn tipo_do_dia(dia: &DiaDaSemana) -> &str {
match dia {
DiaDaSemana::Sabado | DiaDaSemana::Domingo => "Fim de semana",
_ => "Dia útil",
}
}
fn main() {
let hoje = DiaDaSemana::Quarta;
println!("Hoje é: {}", tipo_do_dia(&hoje));
}
Enum com Dados
Esta é uma das funcionalidades mais poderosas do Rust — cada variante pode ter tipos de dados diferentes:
#[derive(Debug)]
enum Forma {
Circulo(f64), // raio
Retangulo(f64, f64), // largura, altura
Triangulo { base: f64, altura: f64 }, // campos nomeados
Ponto, // sem dados
}
impl Forma {
fn area(&self) -> f64 {
match self {
Forma::Circulo(raio) => std::f64::consts::PI * raio * raio,
Forma::Retangulo(largura, altura) => largura * altura,
Forma::Triangulo { base, altura } => base * altura / 2.0,
Forma::Ponto => 0.0,
}
}
fn descricao(&self) -> String {
match self {
Forma::Circulo(r) => format!("Círculo com raio {:.1}", r),
Forma::Retangulo(l, a) => format!("Retângulo {}x{}", l, a),
Forma::Triangulo { base, altura } => {
format!("Triângulo (base: {}, altura: {})", base, altura)
}
Forma::Ponto => String::from("Ponto"),
}
}
}
fn main() {
let formas: Vec<Forma> = vec![
Forma::Circulo(5.0),
Forma::Retangulo(10.0, 3.0),
Forma::Triangulo { base: 8.0, altura: 4.0 },
Forma::Ponto,
];
for forma in &formas {
println!("{}: área = {:.2}", forma.descricao(), forma.area());
}
}
Saída:
Círculo com raio 5.0: área = 78.54
Retângulo 10x3: área = 30.00
Triângulo (base: 8, altura: 4): área = 16.00
Ponto: área = 0.00
Option e Result: Enums da Biblioteca Padrão
Os dois enums mais importantes do Rust:
// Option<T> - representa um valor que pode ou não existir
// enum Option<T> {
// Some(T),
// None,
// }
fn buscar_usuario(id: u32) -> Option<String> {
match id {
1 => Some(String::from("Maria")),
2 => Some(String::from("João")),
_ => None,
}
}
fn main() {
// Option
let usuario = buscar_usuario(1);
match usuario {
Some(nome) => println!("Encontrado: {}", nome),
None => println!("Usuário não encontrado"),
}
let desconhecido = buscar_usuario(99);
match desconhecido {
Some(nome) => println!("Encontrado: {}", nome),
None => println!("Usuário não encontrado"),
}
}
Pattern Matching com match
O match é uma das construções mais poderosas do Rust. Ele verifica exaustivamente todas as possibilidades:
fn classificar_numero(n: i32) -> &'static str {
match n {
0 => "zero",
1..=9 => "um dígito positivo",
10..=99 => "dois dígitos",
100..=999 => "três dígitos",
n if n < 0 => "negativo",
_ => "mil ou mais",
}
}
fn main() {
let numeros = [0, 5, 42, 500, -3, 1000];
for n in numeros {
println!("{}: {}", n, classificar_numero(n));
}
}
Patterns Avançados
#[derive(Debug)]
enum Comando {
Sair,
Mover { x: i32, y: i32 },
Escrever(String),
MudarCor(u8, u8, u8),
}
fn executar(comando: &Comando) {
match comando {
Comando::Sair => {
println!("Saindo do programa...");
}
Comando::Mover { x, y } => {
println!("Movendo para ({}, {})", x, y);
}
Comando::Escrever(texto) => {
println!("Escrevendo: {}", texto);
}
Comando::MudarCor(r, g, b) => {
println!("Cor: RGB({}, {}, {})", r, g, b);
}
}
}
fn main() {
let comandos = vec![
Comando::Mover { x: 10, y: 20 },
Comando::Escrever(String::from("Olá!")),
Comando::MudarCor(255, 128, 0),
Comando::Sair,
];
for cmd in &comandos {
executar(cmd);
}
}
Desestruturação em match
fn main() {
// Desestruturar tuplas
let ponto = (3, -5);
match ponto {
(0, 0) => println!("Na origem"),
(x, 0) => println!("No eixo X em {}", x),
(0, y) => println!("No eixo Y em {}", y),
(x, y) if x > 0 && y > 0 => println!("Primeiro quadrante: ({}, {})", x, y),
(x, y) => println!("Em ({}, {})", x, y),
}
// Match com referências
let texto = String::from("Rust");
match texto.as_str() {
"Rust" => println!("Linguagem de sistemas!"),
"Python" => println!("Linguagem de scripting!"),
outro => println!("Linguagem: {}", outro),
}
// Binding com @
let idade = 25;
match idade {
n @ 0..=12 => println!("Criança de {} anos", n),
n @ 13..=17 => println!("Adolescente de {} anos", n),
n @ 18..=64 => println!("Adulto de {} anos", n),
n => println!("Idoso de {} anos", n),
}
}
if let e while let
Quando você só se importa com uma variante, if let é mais conciso que match:
if let
fn main() {
let valor: Option<i32> = Some(42);
// Com match
match valor {
Some(v) => println!("Valor: {}", v),
None => {}
}
// Com if let (mais conciso!)
if let Some(v) = valor {
println!("Valor: {}", v);
}
// if let com else
let resultado: Result<String, String> = Err(String::from("Arquivo não encontrado"));
if let Ok(conteudo) = resultado {
println!("Conteúdo: {}", conteudo);
} else {
println!("Não foi possível ler o arquivo");
}
// Encadeando if let
let config_porta: Option<u16> = Some(8080);
let config_host: Option<String> = Some(String::from("localhost"));
if let (Some(porta), Some(host)) = (config_porta, &config_host) {
println!("Servidor: {}:{}", host, porta);
}
}
while let
Útil para iterar enquanto um padrão é satisfeito:
fn main() {
let mut pilha = vec![1, 2, 3, 4, 5];
// pop() retorna Option<T>
while let Some(topo) = pilha.pop() {
println!("Desempilhou: {}", topo);
}
println!("Pilha vazia!");
}
Saída:
Desempilhou: 5
Desempilhou: 4
Desempilhou: 3
Desempilhou: 2
Desempilhou: 1
Pilha vazia!
Exemplo Prático: Sistema de Pedidos
Vamos combinar tudo em um exemplo prático:
#[derive(Debug)]
struct Endereco {
rua: String,
cidade: String,
estado: String,
}
#[derive(Debug)]
struct Cliente {
nome: String,
email: String,
}
#[derive(Debug)]
enum MetodoPagamento {
CartaoCredito { numero: String, bandeira: String },
Pix(String), // chave PIX
Boleto,
}
#[derive(Debug)]
enum StatusPedido {
Pendente,
Pago,
Enviado(String), // código de rastreamento
Entregue,
Cancelado(String), // motivo
}
#[derive(Debug)]
struct ItemPedido {
nome: String,
preco: f64,
quantidade: u32,
}
#[derive(Debug)]
struct Pedido {
id: u32,
cliente: Cliente,
endereco: Endereco,
itens: Vec<ItemPedido>,
pagamento: MetodoPagamento,
status: StatusPedido,
}
impl ItemPedido {
fn subtotal(&self) -> f64 {
self.preco * self.quantidade as f64
}
}
impl Pedido {
fn total(&self) -> f64 {
self.itens.iter().map(|item| item.subtotal()).sum()
}
fn resumo_pagamento(&self) -> String {
match &self.pagamento {
MetodoPagamento::CartaoCredito { numero, bandeira } => {
let ultimos4 = &numero[numero.len() - 4..];
format!("Cartão {} final {}", bandeira, ultimos4)
}
MetodoPagamento::Pix(chave) => {
format!("PIX (chave: {})", chave)
}
MetodoPagamento::Boleto => {
String::from("Boleto bancário")
}
}
}
fn resumo_status(&self) -> String {
match &self.status {
StatusPedido::Pendente => String::from("Aguardando pagamento"),
StatusPedido::Pago => String::from("Pagamento confirmado"),
StatusPedido::Enviado(codigo) => {
format!("Enviado - Rastreio: {}", codigo)
}
StatusPedido::Entregue => String::from("Entregue com sucesso"),
StatusPedido::Cancelado(motivo) => {
format!("Cancelado: {}", motivo)
}
}
}
}
fn main() {
let pedido = Pedido {
id: 1001,
cliente: Cliente {
nome: String::from("Ana Costa"),
email: String::from("ana@exemplo.com"),
},
endereco: Endereco {
rua: String::from("Rua das Flores, 123"),
cidade: String::from("São Paulo"),
estado: String::from("SP"),
},
itens: vec![
ItemPedido {
nome: String::from("Livro: Programming Rust"),
preco: 89.90,
quantidade: 1,
},
ItemPedido {
nome: String::from("Adesivo Ferris"),
preco: 5.00,
quantidade: 3,
},
],
pagamento: MetodoPagamento::CartaoCredito {
numero: String::from("4111111111111234"),
bandeira: String::from("Visa"),
},
status: StatusPedido::Enviado(String::from("BR123456789")),
};
println!("=== Pedido #{} ===", pedido.id);
println!("Cliente: {}", pedido.cliente.nome);
println!(
"Endereço: {}, {} - {}",
pedido.endereco.rua, pedido.endereco.cidade, pedido.endereco.estado
);
println!("\nItens:");
for item in &pedido.itens {
println!(
" {} ({}x R$ {:.2}) = R$ {:.2}",
item.nome, item.quantidade, item.preco, item.subtotal()
);
}
println!("\nTotal: R$ {:.2}", pedido.total());
println!("Pagamento: {}", pedido.resumo_pagamento());
println!("Status: {}", pedido.resumo_status());
// Usando if let para verificar status específico
if let StatusPedido::Enviado(codigo) = &pedido.status {
println!("\nRastreie seu pedido: https://rastreamento.exemplo.com/{}", codigo);
}
}
Saída:
=== Pedido #1001 ===
Cliente: Ana Costa
Endereço: Rua das Flores, 123, São Paulo - SP
Itens:
Livro: Programming Rust (1x R$ 89.90) = R$ 89.90
Adesivo Ferris (3x R$ 5.00) = R$ 15.00
Total: R$ 104.90
Pagamento: Cartão Visa final 1234
Status: Enviado - Rastreio: BR123456789
Rastreie seu pedido: https://rastreamento.exemplo.com/BR123456789
Próximos Passos
Com structs, enums e pattern matching, você já tem as ferramentas para modelar domínios complexos de forma segura e expressiva. No próximo tutorial, vamos aprender como lidar com erros de forma elegante em Rust.
Acesse o tutorial Tratamento de Erros para continuar.