terça-feira, 23 de fevereiro de 2021

Usando Structs para Estruturar Dados Relacionados

Uma struct, ou estrutura, é um tipo de dados personalizado que permite nomear e empacotar vários valores relacionados que formam um grupo significativo. Se você está familiarizado com uma linguagem orientada a objetos, uma struct é como os atributos de dados de um objeto. Neste capítulo, vamos comparar e contrastar tuplas com structs, demonstrar como usar structs e discutir como definir métodos e funções associadas para especificar o comportamento associado aos dados. Structs e enums (discutidos no Capítulo 6) são os blocos de construção para a criação de novos tipos no domínio do seu programa para aproveitar ao máximo a verificação de tipo de tempo de compilação do Rust.

Definindo e instanciando structs

As structs são semelhantes às tuplas, que foram discutidas no Capítulo 3. Como as tuplas, as partes de uma structs podem ser de tipos diferentes. Ao contrário das tuplas, você nomeará cada parte dos dados para que fique claro o que os valores significam. Como resultado desses nomes, 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 structs, inserimos a palavra-chave struct e nomeamos toda a estrutura. O nome de uma struct deve descrever a importância das partes dos dados que estão sendo agrupados. Em seguida, dentro das chaves, definimos os nomes e tipos dos dados, que chamamos de campos. Por exemplo, a Listagem 5-1 mostra uma struct que armazena informações sobre uma conta de usuário.

struct Usuario {
    nome_usuario: String,
    email: String,
    contagem_login: u64,
    ativo: bool,
}

fn main() {}

Listagem 5-1: Usuario definição de struct

Para usar uma structs depois de defini-la, criamos uma instância dessa estrutura especificando valores concretos para cada um dos campos. Criamos uma instância informando o nome da estrutura e, em seguida, adicionamos chaves contendo os 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 estrutura. Em outras palavras, a definição da structs é 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 determinado Usuario conforme mostrado na Listagem 5-2.

struct Usuario {
    nome_usuario: String,
    email: String,
    contagem_login: u64,
    ativo: bool,
}

fn main() {
    let user1 = Usuario {
        email: String::from("someone@example.com"),
        nome_usuario: String::from("someusername123"),
        ativo: true,
        contagem_login: 1,
    };
}

Listagem 5-2: Criando uma instância da struct Usuario

Para obter um valor específico de uma estrutura, podemos usar a notação de ponto. Se quiséssemos apenas o endereço de e-mail desse usuário, poderíamos usar user1.emailonde quisermos usar esse valor. 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 emailcampo de uma Usuarioinstância mutável.

struct Usuario {
    nome_usuario: String,
    email: String,
    contagem_login: u64,
    ativo: bool,
}

fn main() {
    let mut user1 = Usuario {
        email: String::from("someone@example.com"),
        nome_usuario: String::from("someusername123"),
        ativo: true,
        contagem_login: 1,
    };

    user1.email = String::from("anotheremail@example.com");
}

Listagem 5-3: Mudando o valor no campo email de uma instância de Usuario

Observe que toda a instância deve ser mutável; Rust não nos permite marcar apenas alguns campos como mutáveis. Como acontece com qualquer expressão, podemos construir uma nova instância da estrutura 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 Usuario com o e-mail e nome de usuário fornecidos. O campo ativo obtém o valor true e o contagem_login obtém o valor 1.

struct Usuario {
    nome_usuario: String,
    email: String,
    contagem_login: u64,
    ativo: bool,
}

fn build_user(email: String, nome_usuario: String) -> Usuario {
    Usuario {
        email: email,
        nome_usuario: nome_usuario,
        ativo: true,
        contagem_login: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}

Listagem 5-4: A função build_user que pega um e-mail e nome de usuário e retorna uma instância de Usuario

Faz sentido para citar os parâmetros da função com o mesmo nome que os campos da struct, mas ter de repetir os nomes de campo e variáveis email e nome_usuario é um pouco tedioso. Se a struct tivesse mais campos, repetir cada um deles seria ainda mais irritante. Felizmente, existe uma abreviatura conveniente!

Usando o atalho de inicialização de campo quando variáveis ​​e campos têm o mesmo nome

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 abreviada do campo init para reescrever build_user de modo que se comporte exatamente da mesma forma, mas não tenha a repetição de email e nome_usuario, conforme mostrado na Listagem 5-5.

struct Usuario {
    nome_usuario: String,
    email: String,
    contagem_login: u64,
    ativo: bool,
}

fn build_user(email: String, nome_usuario: String) -> Usuario {
    Usuario {
        email,
        nome_usuario,
        ativo: true,
        contagem_login: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}

Listagem 5-5: A função build_user usa abreviatura de campo init porque os parâmetros email e nome_usuario têm o mesmo nome que campos os campos da estrutura

Aqui, estamos criando uma nova instância da struct Usuario, 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, precisamos apenas escrever, email em vez de email: email.

Criação de instâncias de outras instâncias com sintaxe de atualização de struct

Muitas vezes é útil criar uma nova instância de uma struct que usa a maioria dos valores de uma instância antiga, mas altera alguns. Você fará isso usando a sintaxe de atualização de struct.

Primeiro, a Listagem 5-6 mostra como criamos uma nova instância de Usuario, user2, sem a sintaxe de atualização. Definimos novos valores para email e nome_usuario mas de outra forma usamos os mesmos valores de user1 que criamos na Listagem 5-2.

struct Usuario {
    nome_usuario: String,
    email: String,
    contagem_login: u64,
    ativo: bool,
}

fn main() {
    let user1 = Usuario {
        email: String::from("someone@example.com"),
        nome_usuario: String::from("someusername123"),
        ativo: true,
        contagem_login: 1,
    };

    let user2 = Usuario {
        email: String::from("another@example.com"),
        nome_usuario: String::from("anotherusername567"),
        ativo: user1.ativo,
        contagem_login: user1.contagem_login,
    };
}

Listagem 5-6: Criando uma nova de instância Usuario usando alguns dos valores de user1

Usando a sintaxe de atualização de structs, podemos obter o mesmo efeito com menos código, conforme 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.

struct Usuario {
    nome_usuario: String,
    email: String,
    contagem_login: u64,
    ativo: bool,
}

fn main() {
    let user1 = Usuario {
        email: String::from("someone@example.com"),
        nome_usuario: String::from("someusername123"),
        ativo: true,
        contagem_login: 1,
    };

    let user2 = Usuario {
        email: String::from("another@example.com"),
        nome_usuario: String::from("anotherusername567"),
        ..user1
    };
}

Listagem 5-7: Usando a sintaxe de atualização de struct para definir novos valores emaile nome_usuariopara uma Usuarioinstância, mas use o restante dos valores dos campos da instância na user1variável

O código na Listagem 5-7 também cria uma instância em user2 que possui um valor diferente para email e nome_usuario mas possui os mesmos valores para os campos ativo e contagem_login de user1.

Usando estruturas de tupla sem campos nomeados para criar tipos diferentes

Você também pode definir structs que se parecem com tuplas, chamadas de estruturas de tupla. As structs de tupla têm o significado adicionado que o nome da estrutura fornece, mas não têm nomes associados a seus campos; em vez disso, eles têm apenas os tipos dos campos. As estruturas de tupla são úteis quando você deseja dar um nome a toda a tupla e torná-la um tipo diferente de outras tuplas, e nomear cada campo como uma estrutura regular seria prolixo ou redundante.

Para definir uma estrutura de tupla, comece com a palavra-chave struct e o nome da estrutura seguido pelos tipos na tupla. Por exemplo, aqui estão as definições e usos de duas structs de tupla chamadas Color e Point:

fn main() {
    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);

    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 estruturas de tupla. Cada estrutura que você define é seu próprio tipo, embora os campos dentro da struct tenham os mesmos tipos. Por exemplo, uma função que recebe um parâmetro do tipo Color não pode aceitar o tipo Point como argumento, embora os dois tipos sejam compostos de três valores i32. Caso contrário, as instâncias de struct de tupla se comportam como tuplas: você pode desestruturá-las em suas partes individuais, pode usar o . seguido pelo índice para acessar um valor individual e assim por diante.

Estruturas semelhantes a unidades sem quaisquer campos

Você também pode definir estruturas que não possuem campos! Eles são chamados de estruturas semelhantes a unidades porque se comportam de maneira semelhante a (), o tipo de unidade. Estruturas semelhantes a unidades podem ser úteis em situações nas quais você precisa implementar uma característica em algum tipo, mas não tem nenhum dado que deseja armazenar no próprio tipo. Discutiremos as características no Capítulo 10.

Propriedade de Struct Data

Na definição da struct Usuario na Listagem 5-1, usamos o tipo String de propriedade em vez do tipo de fatia de string &str. Esta é uma escolha deliberada porque queremos que as instâncias desta estrutura possuam todos os seus dados e que esses dados sejam válidos enquanto a estrutura inteira for válida.

É possível que os structs armazenem referências a dados pertencentes a outra coisa, mas para fazer isso requer o uso de tempos e vida, um recurso Rust que discutiremos no Capítulo 10. Os tempos de vida garantem que os dados referenciados por uma struct sejam válidos por tanto tempo como a estrutura é. Digamos que você tente armazenar uma referência em uma estrutura sem especificar tempos de vida, como este, o que não funcionará:

Nome do arquivo: src / main.rs

Esse código não compila Esse código não compila.

struct Usuario {
    nome_usuario: &str,
    email: &str,
    contagem_login: u64,
    ativo: bool,
}

fn main() {
    let user1 = Usuario {
        email: "someone@example.com",
        nome_usuario: "someusername123",
        ativo: true,
        contagem_login: 1,
    };
}

O compilador reclamará que precisa de especificadores vitalícios:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:2:15
  |
2 |     nome_usuario: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 | struct Usuario<'a> {
2 |     nome_usuario: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:3:12
  |
3 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 | struct Usuario<'a> {
2 |     nome_usuario: &str,
3 |     email: &'a str,
  |

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs`

To learn more, run the command again with --verbose.

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 em Stringvez de referências como &str.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

0 comentários:

Postar um comentário