Introdução
Bem-vindo a Definindo e Instanciando Structs. Este laboratório faz parte do Livro de Rust. Você pode praticar suas habilidades em Rust no LabEx.
Neste laboratório, aprendemos sobre como definir e instanciar structs em Rust, onde as structs armazenam múltiplos valores relacionados e podem ter campos nomeados, permitindo um uso e acesso de dados mais flexível.
Definindo e Instanciando Structs
Structs são semelhantes a tuplas, discutidas em "O Tipo Tupla", pois ambas armazenam múltiplos valores relacionados. Como as tuplas, as partes de uma struct podem ser de tipos diferentes. Diferentemente das tuplas, em uma struct você nomeará cada parte dos dados para que fique claro o que os valores significam. Adicionar esses nomes significa que as structs são mais flexíveis do que as tuplas: você não precisa depender da ordem dos dados para especificar ou acessar os valores de uma instância.
Para definir uma struct, inserimos a palavra-chave struct e nomeamos a struct inteira. O nome de uma struct deve descrever a significância das partes dos dados que estão sendo agrupadas. Então, dentro de chaves, definimos os nomes e tipos das partes dos dados, que chamamos de campos (fields). Por exemplo, a Listagem 5-1 mostra uma struct que armazena informações sobre uma conta de usuário.
Nome do arquivo: src/main.rs
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
Listagem 5-1: Uma definição de struct User
Para usar uma struct depois de defini-la, criamos uma instância (instance) dessa struct especificando valores concretos para cada um dos campos. Criamos uma instância declarando o nome da struct e, em seguida, adicionamos chaves contendo pares chave: valor, onde as chaves são os nomes dos campos e os valores são os dados que queremos armazenar nesses campos. Não precisamos especificar os campos na mesma ordem em que os declaramos na struct. Em outras palavras, a definição da struct é como um modelo geral para o tipo, e as instâncias preenchem esse modelo com dados específicos para criar valores do tipo. Por exemplo, podemos declarar um usuário específico como mostrado na Listagem 5-2.
Nome do arquivo: src/main.rs
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}
Listagem 5-2: Criando uma instância da struct User
Para obter um valor específico de uma struct, usamos a notação de ponto. Por exemplo, para acessar o endereço de e-mail deste usuário, usamos user1.email. Se a instância for mutável, podemos alterar um valor usando a notação de ponto e atribuindo a um campo específico. A Listagem 5-3 mostra como alterar o valor no campo email de uma instância User mutável.
Nome do arquivo: src/main.rs
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
}
Listagem 5-3: Alterando o valor no campo email de uma instância User
Observe que toda a instância deve ser mutável; Rust não nos permite marcar apenas certos campos como mutáveis. Como com qualquer expressão, podemos construir uma nova instância da struct como a última expressão no corpo da função para retornar implicitamente essa nova instância.
A Listagem 5-4 mostra uma função build_user que retorna uma instância User com o e-mail e o nome de usuário fornecidos. O campo active recebe o valor de true, e o sign_in_count recebe um valor de 1.
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
Listagem 5-4: Uma função build_user que recebe um e-mail e um nome de usuário e retorna uma instância User
Faz sentido nomear os parâmetros da função com o mesmo nome dos campos da struct, mas ter que repetir os nomes dos campos e variáveis email e username é um pouco tedioso. Se a struct tivesse mais campos, repetir cada nome ficaria ainda mais irritante. Felizmente, existe uma abreviação conveniente!
Usando a Abreviatura de Inicialização de Campo
Como os nomes dos parâmetros e os nomes dos campos da struct são exatamente os mesmos na Listagem 5-4, podemos usar a sintaxe de abreviação de inicialização de campo (field init shorthand) para reescrever build_user para que se comporte exatamente da mesma forma, mas não tenha a repetição de username e email, como mostrado na Listagem 5-5.
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
Listagem 5-5: Uma função build_user que usa a abreviação de inicialização de campo porque os parâmetros username e email têm o mesmo nome dos campos da struct
Aqui, estamos criando uma nova instância da struct User, que tem um campo chamado email. Queremos definir o valor do campo email para o valor no parâmetro email da função build_user. Como o campo email e o parâmetro email têm o mesmo nome, só precisamos escrever email em vez de email: email.
Criando Instâncias a partir de Outras Instâncias com a Sintaxe de Atualização de Struct
É frequentemente útil criar uma nova instância de uma struct que inclua a maioria dos valores de outra instância, mas altere alguns. Você pode fazer isso usando a sintaxe de atualização de struct (struct update syntax).
Primeiro, na Listagem 5-6, mostramos como criar uma nova instância User em user2 regularmente, sem a sintaxe de atualização. Definimos um novo valor para email, mas, de outra forma, usamos os mesmos valores de user1 que criamos na Listagem 5-2.
Nome do arquivo: src/main.rs
fn main() {
--snip--
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
Listagem 5-6: Criando uma nova instância User usando um dos valores de user1
Usando a sintaxe de atualização de struct, podemos obter o mesmo efeito com menos código, como mostrado na Listagem 5-7. A sintaxe .. especifica que os campos restantes não definidos explicitamente devem ter o mesmo valor que os campos na instância fornecida.
Nome do arquivo: src/main.rs
fn main() {
--snip--
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
Listagem 5-7: Usando a sintaxe de atualização de struct para definir um novo valor de email para uma instância User, mas para usar o restante dos valores de user1
O código na Listagem 5-7 também cria uma instância em user2 que tem um valor diferente para email, mas tem os mesmos valores para os campos username, active e sign_in_count de user1. O ..user1 deve vir por último para especificar que quaisquer campos restantes devem obter seus valores dos campos correspondentes em user1, mas podemos escolher especificar valores para quantos campos quisermos em qualquer ordem, independentemente da ordem dos campos na definição da struct.
Observe que a sintaxe de atualização de struct usa = como uma atribuição; isso ocorre porque ela move os dados, assim como vimos em "Variáveis e Dados Interagindo com Move". Neste exemplo, não podemos mais usar user1 após criar user2 porque a String no campo username de user1 foi movida para user2. Se tivéssemos dado a user2 novos valores String para email e username, e, portanto, usado apenas os valores active e sign_in_count de user1, então user1 ainda seria válido após criar user2. Tanto active quanto sign_in_count são tipos que implementam o trait Copy, então o comportamento que discutimos em "Dados Apenas na Stack: Copy" se aplicaria.
Usando Tuple Structs Sem Campos Nomeados para Criar Tipos Diferentes
Rust também suporta structs que se assemelham a tuplas, chamadas de tuple structs. Tuple structs têm o significado adicional que o nome da struct fornece, mas não têm nomes associados aos seus campos; em vez disso, elas apenas têm os tipos dos campos. Tuple structs são úteis quando você deseja dar um nome à tupla inteira e tornar a tupla um tipo diferente de outras tuplas, e quando nomear cada campo como em uma struct regular seria verboso ou redundante.
Para definir uma tuple struct, comece com a palavra-chave struct e o nome da struct, seguido pelos tipos na tupla. Por exemplo, aqui definimos e usamos duas tuple structs chamadas Color e Point:
Nome do arquivo: src/main.rs
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
Observe que os valores black e origin são tipos diferentes porque são instâncias de diferentes tuple structs. Cada struct que você define é seu próprio tipo, mesmo que os campos dentro da struct possam ter os mesmos tipos. Por exemplo, uma função que recebe um parâmetro do tipo Color não pode receber um Point como argumento, mesmo que ambos os tipos sejam compostos por três valores i32. Caso contrário, as instâncias de tuple struct são semelhantes às tuplas, pois você pode desestruturá-las em suas partes individuais e pode usar um . seguido pelo índice para acessar um valor individual.
Structs Semelhantes a Unidades Sem Nenhum Campo
Você também pode definir structs que não têm nenhum campo! Elas são chamadas de structs semelhantes a unidades porque se comportam de maneira semelhante a (), o tipo unitário que mencionamos em "O Tipo Tupla". Structs semelhantes a unidades podem ser úteis quando você precisa implementar um trait em algum tipo, mas não tem nenhum dado que deseja armazenar no próprio tipo. Discutiremos traits no Capítulo 10. Aqui está um exemplo de como declarar e instanciar uma struct unitária chamada AlwaysEqual:
Nome do arquivo: src/main.rs
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
Para definir AlwaysEqual, usamos a palavra-chave struct, o nome que queremos e, em seguida, um ponto e vírgula. Não há necessidade de chaves ou parênteses! Então, podemos obter uma instância de AlwaysEqual na variável subject de maneira semelhante: usando o nome que definimos, sem chaves ou parênteses. Imagine que, mais tarde, implementaremos um comportamento para este tipo, de modo que cada instância de AlwaysEqual seja sempre igual a cada instância de qualquer outro tipo, talvez para ter um resultado conhecido para fins de teste. Não precisaríamos de nenhum dado para implementar esse comportamento! Você verá no Capítulo 10 como definir traits e implementá-los em qualquer tipo, incluindo structs semelhantes a unidades.
Propriedade dos Dados da Struct
Na definição da struct
Userna Listagem 5-1, usamos o tipoStringpróprio em vez do tipo fatia de string&str. Esta é uma escolha deliberada porque queremos que cada instância desta struct possua todos os seus dados e que esses dados sejam válidos enquanto toda a struct for válida.Também é possível que as structs armazenem referências a dados pertencentes a outra coisa, mas, para fazer isso, é necessário o uso de lifetimes (tempo de vida), um recurso do Rust que discutiremos no Capítulo 10. Lifetimes garantem que os dados referenciados por uma struct sejam válidos enquanto a struct for. Digamos que você tente armazenar uma referência em uma struct sem especificar lifetimes, como o seguinte em
src/main.rs; isso não funcionará:struct User { active: bool, username: &str, email: &str, sign_in_count: u64, } fn main() { let user1 = User { active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1, }; }O compilador reclamará que precisa de especificadores de tempo de vida:
$ `cargo run` Compiling structs v0.1.0 (file:///projects/structs) error[E0106]: missing lifetime specifier --> src/main.rs:3:15 | 3 | username: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 ~ username: &'a str, | error[E0106]: missing lifetime specifier --> src/main.rs:4:12 | 4 | email: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 | username: &str, 4 ~ email: &'a str, |No Capítulo 10, discutiremos como corrigir esses erros para que você possa armazenar referências em structs, mas, por enquanto, corrigiremos erros como esses usando tipos próprios como
Stringem vez de referências como&str.
Resumo
Parabéns! Você concluiu o laboratório Definindo e Instanciando Structs. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.