Rust

Structs — Criando seus próprios tipos de dados Já leu

8 min de leitura

Structs — Criando seus próprios tipos de dados
  Até agora trabalhamos com tipos primitivos — inteiros, strings, tuplas. Mas programas reais precisam representar conceitos do mundo real: u

 

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 #09. Por ora, use String em campos de structs e &str em parâmetros de funções.


Fontes e leituras recomendadas

Comentários

Mais em Rust

Enums e Pattern Matching — O Sistema Mais Expressivo que Você Já Viu
Enums e Pattern Matching — O Sistema Mais Expressivo que Você Já Viu

&nbsp; Se structs s&atilde;o a forma de Rust agrupar dados relacionados,&nbs...

Tratamento de Erros com Result — Erros como Valores, não Exceções
Tratamento de Erros com Result — Erros como Valores, não Exceções

&nbsp; Em linguagens como Java, Python e C#, erros s&atilde;o tratados com e...

Closures Avançadas e Programação Funcional — Indo Além do map e filter
Closures Avançadas e Programação Funcional — Indo Além do map e filter

&nbsp; Em um artigo anterior, introduzimos closures e os adaptadores mais co...