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 Stringusuario1 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

Controle de Fluxo — if, loop, while e for como você nunca viu antes
Controle de Fluxo — if, loop, while e for como você nunca viu antes

Se o if produz valor, a construção que outras linguagens resolvem com ternário…

Rust para Data Science — Processamento de Dados com Polars e ndarray
Rust para Data Science — Processamento de Dados com Polars e ndarray

Rust entra em processamento de dados quando o custo de execução pesa mais que…

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…