Structs — Criando seus próprios tipos de dados

Structs — Criando seus próprios tipos de dados

Agrupar dados relacionados num tipo próprio é o que separa um programa de um punhado de variáveis soltas. O artigo cobre a definição e a instanciação, os atalhos de inicialização, os métodos e as funções associadas que servem de construtor, e o que muda quando a struct passa a ser dona de uma String.
Rust

• • 12 min de leitura

 

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 dados
  • self — 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 Tratamento de Erros com Result — Erros como Valores, não Exceções. Por ora, use String em campos de structs e &str em parâmetros de funções.

Exercícios

Exercício 1

Defina uma struct Retangulo com largura e altura, e implemente um método area e uma função associada quadrado que construa um retângulo de lados iguais. Explique a diferença entre os dois.

Ver resposta

✓ Resposta:

struct Retangulo {
    largura: u32,
    altura: u32,
}

impl Retangulo {
    fn area(&self) -> u32 {
        self.largura * self.altura
    }

    fn quadrado(lado: u32) -> Self {
        Self { largura: lado, altura: lado }
    }
}

area é um método: recebe &self e é chamado sobre uma instância (r.area()). quadrado é uma função associada: não recebe self, pertence ao tipo e é chamada com Retangulo::quadrado(5). É a convenção de construtor em Rust — a linguagem não tem construtor especial, e new é apenas um nome combinado.

Exercício 2

Explique quando usar &self, &mut self e self num método, com um exemplo de cada.

Ver resposta

✓ Resposta: &self lê sem alterar nem tomar posse — é o caso mais comum, como um area(). &mut self altera a instância, como um redimensionar(&mut self, l: u32); exige que quem chama tenha a variável declarada como mut. self por valor consome a instância, que deixa de existir para quem chamou — usado quando o método transforma o objeto em outra coisa, como um into_partes(self) -> (u32, u32). A escolha comunica a intenção: quem lê a assinatura já sabe se o objeto continua utilizável depois.

Exercício 3

O código não compila. Explique e corrija.

struct Conta { saldo: f64 }

impl Conta {
    fn depositar(&self, valor: f64) {
        self.saldo += valor;
    }
}
Ver resposta

✓ Resposta: O método altera self.saldo, mas recebeu &self, que é um empréstimo imutável — o erro é cannot assign to self.saldo, which is behind a & reference. A correção é declarar a intenção de modificar:

fn depositar(&mut self, valor: f64) {
    self.saldo += valor;
}

E quem chama precisa de uma variável mutável: let mut conta = Conta { saldo: 0.0 };. Note que a mutabilidade não é propriedade do campo, e sim de como o valor é acessado — Rust não tem o equivalente a um campo const isolado.

Exercício 4

Explique o que #[derive(Debug)] faz e por que println!("{}", retangulo) não compila para uma struct comum.

Ver resposta

✓ Resposta: {} exige o trait Display, que Rust não gera automaticamente: como uma struct pode ser exibida de muitas formas, a linguagem obriga a decidir implementando Display à mão. Já {:?} usa Debug, voltado à depuração, e esse pode ser derivado:

#[derive(Debug)]
struct Retangulo { largura: u32, altura: u32 }

println!("{:?}", r);    // Retangulo { largura: 3, altura: 4 }
println!("{:#?}", r);   // mesma coisa, indentado em várias linhas

O derive gera a implementação em tempo de compilação a partir dos campos, desde que todos eles também implementem Debug.

Exercício 5

O que acontece ao guardar uma String dentro de uma struct e passar a struct para uma função por valor? Mostre o problema e a solução.

Ver resposta

✓ Resposta: A struct passa a ser dona da String, então passá-la por valor move a struct inteira — e o chamador perde o acesso:

struct Usuario { nome: String }

fn exibir(u: Usuario) { println!("{}", u.nome); }

fn main() {
    let u = Usuario { nome: String::from("Ana") };
    exibir(u);
    // println!("{}", u.nome);  // erro: valor movido
}

A solução é emprestar, mudando a assinatura para fn exibir(u: &Usuario) e chamando com exibir(&u). Tomar posse só faz sentido quando a função realmente consome o valor — por exemplo, ao guardá-lo numa coleção.

Fontes e leituras recomendadas

Comentários

Mais em Rust

Banco de Dados com SQLx — Persistência Real para sua API
Banco de Dados com SQLx — Persistência Real para sua API

O SQLx não esconde o banco atrás de objetos: o SQL continua SQL, e as macros o…

Testes — Escrevendo Código que Prova que Seu Código Funciona
Testes — Escrevendo Código que Prova que Seu Código Funciona

Teste em Rust mora junto do código que verifica, num módulo compilado apenas…

Construindo uma API REST com Axum
Construindo uma API REST com Axum

Uma API REST em Rust começa por entender extratores: tipos que sabem se…