Structs em Rust
LidoNesta aula, você aprenderá a definir e usar structs em Rust, uma das formas mais importantes de criar tipos de dados personalizados. Vamos explorar a sintaxe básica, como instanciar structs, acessar e modificar campos, além da conveniente update syntax para reutilizar valores de outras instâncias. Ao final, você terá uma base sólida para modelar dados complexos em Rust.
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 ativoUma 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 comprintln!. - 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
Defina uma struct chamada
Livrocom os campostitulo(String),autor(String),paginas(u32) edisponivel(bool). Em seguida, crie uma instância chamadameu_livrocom 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, };Escreva uma função
criar_livroque receba título, autor e páginas como parâmetros e retorne umLivrocomdisponivelinicializado comotrue. Use field init shorthand.✓ Resposta:fn criar_livro(titulo: String, autor: String, paginas: u32) -> Livro { Livro { titulo, autor, paginas, disponivel: true, } }Usando a struct
Livroe a instânciameu_livrodo exercício 1, crie uma nova instânciaoutro_livroque 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 };Explique o que acontece com a variável
meu_livroapós o uso na update syntax do exercício 3, considerando queStringnão implementaCopy. O que você poderia fazer para ainda usarmeu_livrodepois?✓ Resposta: ComoStringnão éCopy, a update syntax move os campos não especificados demeu_livroparaoutro_livro. Portanto,meu_livronão pode mais ser usado (o compilador emitirá um erro). Para evitar isso, você pode clonar os campos explicitamente ou usar o métodoclone()nos campos antes de mover, por exemplo:..meu_livro.clone()(se a struct derivarClone). Outra opção é implementarCopypara a struct, mas isso só é possível se todos os campos foremCopy.Crie uma struct
Retangulocom camposlarguraealtura(ambos u32). Implemente um métodoareaque 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 }