Até agora trabalhamos com tipos primitivos — inteiros, strings, tuplas. Mas programas reais precisam representar conceitos do mundo real: um usuário, um pedido, uma conta bancária, um ponto no espaço. Para isso, Rust oferece as structs — estruturas que agrupam dados relacionados sob um único nome com significado.
Se você vem de linguagens orientadas a objetos, vai reconhecer semelhanças com classes — mas vai notar diferenças importantes. Rust não tem herança. Em vez disso, tem composição, traits e um sistema de tipos que favorece explicitidade. Veremos tudo isso ao longo da série. Por hoje, o foco é nas structs em si.
Definindo e instanciando uma struct
Uma struct é definida com a palavra-chave struct, seguida do nome e dos campos com seus tipos:
struct Usuario {
nome: String,
email: String,
idade: u32,
ativo: bool,
}
fn main() {
let usuario1 = Usuario {
nome: String::from("Ana Silva"),
email: String::from("ana@exemplo.com"),
idade: 30,
ativo: true,
};
println!("Usuário: {}", usuario1.nome);
println!("Email: {}", usuario1.email);
println!("Idade: {}", usuario1.idade);
}
Para acessar os campos, usa-se a notação de ponto. Simples e direto.
Para modificar campos, a instância inteira precisa ser mutável — Rust não permite marcar campos individuais como mutáveis:
fn main() {
let mut usuario1 = Usuario {
nome: String::from("Ana Silva"),
email: String::from("ana@exemplo.com"),
idade: 30,
ativo: true,
};
usuario1.email = String::from("ana.silva@exemplo.com");
println!("Novo email: {}", usuario1.email);
}
Field Init Shorthand
Quando o nome do parâmetro de uma função é igual ao nome do campo da struct, você não precisa repetir:
fn criar_usuario(nome: String, email: String, idade: u32) -> Usuario {
Usuario {
nome, // equivale a nome: nome
email, // equivale a email: email
idade,
ativo: true,
}
}
fn main() {
let u = criar_usuario(
String::from("Carlos"),
String::from("carlos@exemplo.com"),
25,
);
println!("{} — {}", u.nome, u.email);
}
Essa sintaxe reduz repetição sem sacrificar clareza — um equilíbrio que o design de Rust persegue constantemente.
Struct Update Syntax
Frequentemente você precisa criar uma nova instância baseada em outra, mudando apenas alguns campos:
fn main() {
let usuario1 = criar_usuario(
String::from("Ana"),
String::from("ana@exemplo.com"),
30,
);
let usuario2 = Usuario {
email: String::from("outro@exemplo.com"),
..usuario1 // demais campos vêm de usuario1
};
println!("{} — {}", usuario2.nome, usuario2.email);
}
O ..usuario1 diz: "use os valores restantes de usuario1". Atenção: isso é um move. Se algum campo movido for do tipo que não implementa Copy — como String — usuario1 ficará inválido após isso. Campos copiados com Copy, como u32 e bool, não causam esse problema.
Tuple Structs — structs sem nomes de campo
Às vezes você quer criar um tipo distinto mas sem a verbosidade de nomear cada campo. Para isso existem as tuple structs:
struct Ponto(f64, f64, f64);
struct Cor(u8, u8, u8);
fn main() {
let origem = Ponto(0.0, 0.0, 0.0);
let vermelho = Cor(255, 0, 0);
println!("x={}, y={}, z={}", origem.0, origem.1, origem.2);
println!("R={}, G={}, B={}", vermelho.0, vermelho.1, vermelho.2);
}
Ponto e Cor são tipos diferentes mesmo tendo a mesma estrutura interna. Uma função que espera Ponto não aceita Cor — o compilador distingue os dois. Isso evita confusão de parâmetros em chamadas de função.
Unit Structs — structs sem dados
Existe ainda um terceiro tipo: structs sem nenhum campo. São chamadas de unit structs e serão muito úteis quando explorarmos traits:
struct Marcador;
fn main() {
let _m = Marcador;
}
Por ora, guarde que elas existem. Voltaremos a elas quando falarmos em traits e generics.
Métodos — funções dentro de structs
Structs em Rust podem ter métodos — funções associadas a um tipo específico. Eles são definidos dentro de um bloco impl:
struct Retangulo {
largura: f64,
altura: f64,
}
impl Retangulo {
fn area(&self) -> f64 {
self.largura * self.altura
}
fn perimetro(&self) -> f64 {
2.0 * (self.largura + self.altura)
}
fn e_quadrado(&self) -> bool {
self.largura == self.altura
}
}
fn main() {
let r = Retangulo {
largura: 5.0,
altura: 3.0,
};
println!("Área: {}", r.area());
println!("Perímetro: {}", r.perimetro());
println!("É quadrado? {}", r.e_quadrado());
}
Saída:
Área: 15
Perímetro: 16
É quadrado? false
O primeiro parâmetro dos métodos é sempre self — a referência para a própria instância. As formas mais comuns são:
&self— lê os dados sem tomar posse e sem modificar&mut self— lê e modifica os dadosself— toma posse da instância, consumindo-a
Na prática, &self é o mais usado. self sem referência é raro e indica que o método consome a instância — útil em padrões de transformação que veremos mais adiante.
Métodos que modificam — &mut self
struct Contador {
valor: u32,
limite: u32,
}
impl Contador {
fn incrementar(&mut self) {
if self.valor < self.limite {
self.valor += 1;
}
}
fn resetar(&mut self) {
self.valor = 0;
}
fn atual(&self) -> u32 {
self.valor
}
}
fn main() {
let mut c = Contador { valor: 0, limite: 3 };
c.incrementar();
c.incrementar();
c.incrementar();
c.incrementar(); // não passa do limite
println!("Valor atual: {}", c.atual()); // 3
c.resetar();
println!("Após reset: {}", c.atual()); // 0
}
Funções associadas — construtores por convenção
Além de métodos, um bloco impl pode conter funções associadas — funções que pertencem ao tipo mas não recebem self. São chamadas com :: em vez de .:
impl Retangulo {
fn novo(largura: f64, altura: f64) -> Retangulo {
Retangulo { largura, altura }
}
fn quadrado(lado: f64) -> Retangulo {
Retangulo {
largura: lado,
altura: lado,
}
}
}
fn main() {
let r = Retangulo::novo(10.0, 5.0);
let q = Retangulo::quadrado(4.0);
println!("Retângulo: {}×{}", r.largura, r.altura);
println!("Quadrado: {}×{}", q.largura, q.altura);
println!("É quadrado? {}", q.e_quadrado());
}
Por convenção, a função novo ou new funciona como construtor. Rust não tem construtores especiais — são apenas funções associadas com um nome convencional.
Múltiplos blocos impl
Uma struct pode ter múltiplos blocos impl. Isso é útil para organizar métodos por categoria ou ao implementar traits:
impl Retangulo {
fn area(&self) -> f64 { self.largura * self.altura }
}
impl Retangulo {
fn perimetro(&self) -> f64 {
2.0 * (self.largura + self.altura)
}
}
Ambos os blocos são válidos e complementares. O compilador os trata como um só.
Exibindo structs com Debug
Por padrão, você não pode imprimir uma struct com println!. Você precisa derivar o trait Debug:
#[derive(Debug)]
struct Ponto {
x: f64,
y: f64,
}
fn main() {
let p = Ponto { x: 3.0, y: -1.5 };
println!("{:?}", p); // compacto: Ponto { x: 3.0, y: -1.5 }
println!("{:#?}", p); // expandido:
// Ponto {
// x: 3.0,
// y: -1.5,
// }
}
O #[derive(Debug)] é uma macro de derivação — instrui o compilador a gerar automaticamente a implementação de Debug para a struct. É a forma idiomática de tornar structs inspecionáveis durante o desenvolvimento.
Um programa completo: biblioteca de livros
Vamos reunir tudo em um exemplo coeso:
#[derive(Debug)]
struct Livro {
titulo: String,
autor: String,
paginas: u32,
lido: bool,
}
impl Livro {
fn novo(titulo: &str, autor: &str, paginas: u32) -> Livro {
Livro {
titulo: String::from(titulo),
autor: String::from(autor),
paginas,
lido: false,
}
}
fn marcar_lido(&mut self) {
self.lido = true;
println!("'{}' marcado como lido!", self.titulo);
}
fn resumo(&self) -> String {
let status = if self.lido { "✓ Lido" } else { "○ Não lido" };
format!(
"[{}] '{}' — {} ({} páginas)",
status, self.titulo, self.autor, self.paginas
)
}
}
fn main() {
let mut biblioteca = vec![
Livro::novo("The Rust Programming Language", "Steve Klabnik", 526),
Livro::novo("Programming Rust", "Jim Blandy", 622),
Livro::novo("Rust in Action", "Tim McNamara", 456),
];
biblioteca[0].marcar_lido();
biblioteca[2].marcar_lido();
println!("\n── Minha Biblioteca ──");
for livro in &biblioteca {
println!("{}", livro.resumo());
}
let lidos = biblioteca.iter().filter(|l| l.lido).count();
println!("\n{}/{} livros lidos.", lidos, biblioteca.len());
}
Saída:
'The Rust Programming Language' marcado como lido!
'Rust in Action' marcado como lido!
── Minha Biblioteca ──
[✓ Lido] 'The Rust Programming Language' — Steve Klabnik (526 páginas)
[○ Não lido] 'Programming Rust' — Jim Blandy (622 páginas)
[✓ Lido] 'Rust in Action' — Tim McNamara (456 páginas)
2/3 livros lidos.
Ownership dentro de structs
Um detalhe importante: os campos da struct Usuario que criamos no início usam String, não &str. Isso é intencional — a struct precisa possuir seus dados. Se usássemos referências, o compilador exigiria lifetime annotations para garantir que os dados referenciados vivam pelo menos tanto quanto a struct. Esse é um tópico avançado que exploraremos no Artigo #09. Por ora, use String em campos de structs e &str em parâmetros de funções.
Fontes e leituras recomendadas
- The Rust Programming Language, Cap. 5 — Using Structs to Structure Related Data — https://doc.rust-lang.org/book/ch05-00-structs.html
- Rust by Example — Structures — https://doc.rust-lang.org/rust-by-example/custom_types/structs.html
- Rust by Example — Methods — https://doc.rust-lang.org/rust-by-example/fn/methods.html
- Rust API Guidelines — Naming — convenções para construtores e métodos — https://rust-lang.github.io/api-guidelines/naming.html
- Rustlings, seção
structs— https://github.com/rust-lang/rustlings