Structs (abreviação de structures) são a espinha dorsal da modelagem de dados em Rust. Elas permitem agrupar valores relacionados de diferentes tipos em uma única unidade nomeada, tornando seu código mais organizado, legível e expressivo. Nesta aula, vamos mergulhar fundo na sintaxe das structs, desde a definição até a instanciação, e explorar recursos como a update syntax que facilitam a criação de novas instâncias a partir de outras.

Dominar structs é essencial para qualquer programa Rust não trivial, pois elas são usadas em praticamente todos os projetos, desde simples scripts até sistemas robustos. Além disso, structs são a base para implementar comportamentos via traits, que veremos em aulas futuras. Prepare-se para uma aula prática e completa, com exemplos que você pode testar imediatamente.

Definindo structs

Em Rust, uma struct é definida com a palavra-chave struct, seguida do nome da struct (em PascalCase) e um corpo delimitado por chaves. Dentro do corpo, listamos os campos, cada um com um nome (snake_case) e um tipo. Por exemplo, podemos definir uma struct para representar um usuário de um sistema:

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

Essa definição declara que um User tem quatro campos: username e email (Strings), sign_in_count (um inteiro sem sinal de 64 bits) e active (um booleano). Note que os campos são separados por vírgulas, e a vírgula após o último campo é opcional (embora seja comum incluí-la).

É importante notar que definir uma struct não aloca memória; apenas cria um novo tipo. Para usar a struct, precisamos criar instâncias dela, como veremos na próxima seção. Além disso, Rust não permite que uma struct seja definida sem pelo menos um campo (a menos que seja uma unit struct, que não tem campos e é usada em casos específicos).

Instanciando

Para criar uma instância de uma struct, usamos a sintaxe de inicialização: nome da struct, seguido de chaves com os valores de cada campo na forma campo: valor. A ordem dos campos não importa, mas todos os campos devem ser especificados. Por exemplo:

let user1 = User {
    username: String::from("alice"),
    email: String::from("alice@example.com"),
    sign_in_count: 1,
    active: true,
};

Aqui, criamos uma instância chamada user1 do tipo User. Cada campo é inicializado com um valor. Note que usamos String::from para criar strings, pois literais de string ("alice") são do tipo &str, que não é o mesmo que String.

Também é possível criar uma instância mutável, permitindo modificar campos posteriormente. Para isso, usamos a palavra-chave mut:

let mut user2 = User {
    username: String::from("bob"),
    email: String::from("bob@example.com"),
    sign_in_count: 1,
    active: false,
};
user2.active = true; // agora ativo

Uma técnica comum é criar uma função construtora (normalmente chamada de new) que retorna uma nova instância com valores padrão. Por exemplo:

impl User {
    fn new(username: String, email: String) -> User {
        User {
            username,
            email,
            sign_in_count: 1,
            active: true,
        }
    }
}

Note que, no corpo da função, usamos a forma abreviada para campos cujo valor vem de uma variável com o mesmo nome. Em vez de username: username, podemos escrever apenas username. Isso é chamado de field init shorthand e torna o código mais limpo.

Campos

Os campos de uma struct são acessados com a notação de ponto. Por exemplo, para obter o e-mail de um usuário, fazemos user1.email. Se a instância for mutável, podemos atribuir um novo valor ao campo: user1.email = String::from("novo@example.com").

Além disso, podemos usar a sintaxe de atualização (update syntax) para criar uma nova instância baseada em outra, alterando apenas alguns campos. Isso é útil quando queremos copiar a maioria dos valores. A update syntax usa .. seguido da instância que fornece os valores restantes. Por exemplo:

let user3 = User {
    email: String::from("carol@example.com"),
    ..user1
};

Nesse caso, user3 terá o mesmo username, sign_in_count e active que user1, mas o email será o novo valor. A regra é que os campos explicitamente definidos antes de .. substituem os da instância original. Os campos restantes são movidos (se forem tipos que não implementam Copy) da instância original. Portanto, após essa operação, user1 não pode mais ser usado se contiver campos não-Copy (como String). Para evitar a movimentação, poderíamos clonar os dados explicitamente.

É importante notar que a update syntax é uma forma sintática para criar uma nova struct, não uma mutação da original. Ela é equivalente a escrever todos os campos manualmente, mas economiza digitação e reduz erros.

Update syntax

A update syntax é um recurso elegante para criar novas instâncias a partir de outras, como vimos no exemplo anterior. Ela é especialmente útil quando temos um struct com muitos campos e queremos apenas alterar um ou dois. Sem ela, teríamos que repetir todos os campos, o que é propenso a erros.

Vamos ver um exemplo mais completo. Suponha que temos uma struct Ponto com coordenadas x e y:

#[derive(Debug)]
struct Ponto {
    x: i32,
    y: i32,
}

let ponto_original = Ponto { x: 10, y: 20 };
let ponto_modificado = Ponto { x: 30, ..ponto_original };
println!("Original: {:?}", ponto_original); // ainda acessível, pois i32 é Copy
println!("Modificado: {:?}", ponto_modificado);

Como i32 implementa a trait Copy, a instância ponto_original continua válida após o uso na update syntax. Se os campos fossem String, a instância original seria movida e não poderia ser usada depois. Portanto, ao usar update syntax, tenha em mente a propriedade (ownership) e as traits Copy vs Move.

A update syntax também pode ser usada para criar uma nova instância que copia exatamente outra, sem alterar nenhum campo: let copia = Ponto { ..ponto_original };. Isso é semelhante a uma cópia superficial, mas novamente, se houver campos não-Copy, a original será movida.

Boas práticas e observações

Ao usar structs, siga estas recomendações:

  • Use nomes descritivos em PascalCase para as structs e snake_case para campos e funções.
  • Implemente a trait Debug (via derive) para facilitar a depuração com println!.
  • Considere usar o field init shorthand para reduzir redundância.
  • Lembre-se de que structs são tipos de valor: quando você passa uma struct para uma função, ela é movida (ou copiada se implementar Copy).
  • Use a update syntax com cuidado para não mover dados indesejadamente.

Exercícios

  1. Defina uma struct chamada Livro com os campos titulo (String), autor (String), paginas (u32) e disponivel (bool). Em seguida, crie uma instância chamada meu_livro com valores de sua escolha.

    ✓ Resposta:
    struct Livro {
        titulo: String,
        autor: String,
        paginas: u32,
        disponivel: bool,
    }
    
    let meu_livro = Livro {
        titulo: String::from("O Senhor dos Anéis"),
        autor: String::from("J.R.R. Tolkien"),
        paginas: 1200,
        disponivel: true,
    };
  2. Escreva uma função criar_livro que receba título, autor e páginas como parâmetros e retorne um Livro com disponivel inicializado como true. Use field init shorthand.

    ✓ Resposta:
    fn criar_livro(titulo: String, autor: String, paginas: u32) -> Livro {
        Livro {
            titulo,
            autor,
            paginas,
            disponivel: true,
        }
    }
  3. Usando a struct Livro e a instância meu_livro do exercício 1, crie uma nova instância outro_livro que tenha o mesmo título e autor, mas 500 páginas e disponível como false. Use update syntax.

    ✓ Resposta:
    let outro_livro = Livro {
        paginas: 500,
        disponivel: false,
        ..meu_livro
    };
  4. Explique o que acontece com a variável meu_livro após o uso na update syntax do exercício 3, considerando que String não implementa Copy. O que você poderia fazer para ainda usar meu_livro depois?

    ✓ Resposta: Como String não é Copy, a update syntax move os campos não especificados de meu_livro para outro_livro. Portanto, meu_livro não pode mais ser usado (o compilador emitirá um erro). Para evitar isso, você pode clonar os campos explicitamente ou usar o método clone() nos campos antes de mover, por exemplo: ..meu_livro.clone() (se a struct derivar Clone). Outra opção é implementar Copy para a struct, mas isso só é possível se todos os campos forem Copy.
  5. Crie uma struct Retangulo com campos largura e altura (ambos u32). Implemente um método area que calcula a área. Teste com um retângulo de 30x50 e imprima a área.

    ✓ Resposta:
    struct Retangulo {
        largura: u32,
        altura: u32,
    }
    
    impl Retangulo {
        fn area(&self) -> u32 {
            self.largura * self.altura
        }
    }
    
    fn main() {
        let ret = Retangulo { largura: 30, altura: 50 };
        println!("Área: {}", ret.area()); // 1500
    }

Referências