Definição e Conceitos Fundamentais
Structs (estruturas) são um dos pilares da programação orientada a dados em Rust. Elas permitem agrupar múltiplos valores relacionados em uma única entidade nomeada, cada um com seu próprio tipo. Diferentemente de tuplas, os campos de uma struct possuem nomes descritivos, tornando o código mais legível e mantível. Rust oferece três tipos de structs: com campos nomeados, com campos sem nome (tuple struct) e sem campos (unit struct).
A declaração básica de uma struct é simples. Você define a estrutura com a palavra-chave struct, seguida do nome e seus campos. Cada campo deve ter um tipo explícito anotado. Uma struct é um tipo produto — combina múltiplos tipos em um único tipo composto. Ao contrário de enums (tipos soma), uma struct contém sempre todos os seus campos, nunca apenas alguns.
// Struct com campos nomeados
struct Pessoa {
nome: String,
idade: u32,
email: String,
}
// Tuple struct (campos sem nome)
struct Ponto(f64, f64);
// Unit struct (sem campos)
struct Marcador;
Instanciação de Structs
Para usar uma struct, você precisa criar uma instância fornecendo valores para todos os campos. Rust exige que todos os campos sejam inicializados explicitamente — não há valores padrão implícitos. A sintaxe é intuitiva: nome da struct seguido de chaves contendo pares campo-valor.
A instanciação segue o princípio de propriedade do Rust. Valores movidos para a struct se tornam propriedade dela. Se você passar uma String para um campo, ela não poderá mais ser usada na variável original sem clonagem. Para structs que implementam Copy (como números inteiros), isso não é problema pois eles são copiados automaticamente.
fn main() {
// Instanciação simples
let pessoa1 = Pessoa {
nome: String::from("Alice"),
idade: 28,
email: String::from("alice@example.com"),
};
// Sintaxe de campo abreviado
let nome = String::from("Bob");
let idade = 35;
let email = String::from("bob@example.com");
let pessoa2 = Pessoa {
nome, // equivalente a nome: nome
idade, // equivalente a idade: idade
email,
};
// Usando função construtora
let pessoa3 = criar_pessoa(
"Carlos",
30,
"carlos@example.com",
);
// Tuple struct
let ponto = Ponto(3.14, 2.71);
println!("X: {}, Y: {}", ponto.0, ponto.1);
// Acessando campos
println!("{} tem {} anos", pessoa1.nome, pessoa1.idade);
}
fn criar_pessoa(nome: &str, idade: u32, email: &str) -> Pessoa {
Pessoa {
nome: String::from(nome),
idade,
email: String::from(email),
}
}
Uma prática recomendada é criar funções associadas que atuam como construtores. Isso encapsula a lógica de inicialização e torna o código mais seguro e flexível para futuras mudanças.
Métodos e Funções Associadas
Métodos são funções vinculadas a uma struct através do bloco impl. Eles diferem de funções simples porque recebem self (ou &self, &mut self) como primeiro parâmetro, representando uma instância da struct. Funções associadas não recebem self e são geralmente usadas como construtores.
O sistema de ownership do Rust se integra perfeitamente com métodos. Um método pode receber &self (empréstimo imutável), &mut self (empréstimo mutável) ou self (posse total). Escolher o tipo correto é crucial para permitir composição e reutilização de código sem erros de borrow.
impl Pessoa {
// Função associada (construtor padrão)
fn nova(nome: &str, idade: u32, email: &str) -> Self {
Pessoa {
nome: String::from(nome),
idade,
email: String::from(email),
}
}
// Método que pega empréstimo imutável
fn apresentar(&self) {
println!(
"Olá, meu nome é {} e tenho {} anos",
self.nome, self.idade
);
}
// Método que retorna informações
fn eh_maior_de_idade(&self) -> bool {
self.idade >= 18
}
// Método que pega empréstimo mutável
fn fazer_aniversario(&mut self) {
self.idade += 1;
println!("{} agora tem {} anos", self.nome, self.idade);
}
// Método que consome self
fn converter_em_string(self) -> String {
format!(
"{} ({} anos) - {}",
self.nome, self.idade, self.email
)
}
}
impl Ponto {
fn distancia_origem(&self) -> f64 {
(self.0.powi(2) + self.1.powi(2)).sqrt()
}
fn deslocar(&mut self, dx: f64, dy: f64) {
self.0 += dx;
self.1 += dy;
}
}
fn main() {
let mut pessoa = Pessoa::nova("Diana", 25, "diana@example.com");
pessoa.apresentar();
println!("Maior de idade? {}", pessoa.eh_maior_de_idade());
pessoa.fazer_aniversario();
// Isso consome pessoa — não pode mais ser usada depois
let resumo = pessoa.converter_em_string();
println!("{}", resumo);
let mut ponto = Ponto(3.0, 4.0);
println!("Distância: {}", ponto.distancia_origem());
ponto.deslocar(1.0, 1.0);
println!("Novo ponto: ({}, {})", ponto.0, ponto.1);
}
Boas Práticas e Padrões Comuns
Estruturar bem suas structs é fundamental para código limpo e eficiente. Use structs para agrupar dados relacionados, não como simples containers de valores sem contexto. Implemente construtores através de funções associadas (new é a convenção) para facilitar a criação de instâncias. Considere derivar traits úteis como Debug, Clone e Copy quando apropriado.
Um padrão poderoso é usar impl separadamente para diferentes grupos de métodos. Você pode ter múltiplos blocos impl para a mesma struct, facilitando organização lógica. Lembre-se: Rust força você a pensar sobre ownership de forma explícita, o que previne bugs sutis em outras linguagens. Abraçe isso ao desenhar seus tipos.
#[derive(Debug, Clone)]
struct Retangulo {
largura: f64,
altura: f64,
}
impl Retangulo {
fn novo(largura: f64, altura: f64) -> Self {
Retangulo { largura, altura }
}
}
// Separar grupos de funcionalidade
impl Retangulo {
fn area(&self) -> f64 {
self.largura * self.altura
}
fn perimetro(&self) -> f64 {
2.0 * (self.largura + self.altura)
}
}
impl Retangulo {
fn pode_conter(&self, outro: &Retangulo) -> bool {
self.largura >= outro.largura && self.altura >= outro.altura
}
}
fn main() {
let ret1 = Retangulo::novo(30.0, 50.0);
let ret2 = ret1.clone();
println!("Área: {}", ret1.area());
println!("Perímetro: {}", ret1.perimetro());
println!("Contém ret2? {}", ret1.pode_conter(&ret2));
}
Conclusão
Nesta aula, dominamos os três elementos essenciais de structs em Rust: definição clara de tipos compostos, instanciação com ownership seguro e métodos integrados ao sistema de borrow. Structs são a base para escrever abstrações robustas e performáticas. O sistema de tipos de Rust torna impossível criar bugs sutis com dados — tudo é verificado em tempo de compilação.
Pratique criando structs para seus próprios domínios: usuários, produtos, eventos. Experimente com diferentes combinações de self, &self e &mut self até internalizar quando cada um faz sentido. Com structs bem desenhadas, você escreverá código que é simultaneamente seguro, eficiente e expressivo.