Conhecendo Rust — Parte 1 — Structs, Impl e Traits

Jonathan
7 min readJan 24, 2021

--

E lá vamos de parte 1, hoje vamos tratar de Structs, Impl e Traits, mas ainda não espere irmos muito a fundo nessas partes iniciais.

Nesse artigo estaremos utilizando as seguintes crates:

Structs

Para quem está acostumado com C/C++ e Go, já deve saber o que são structs. Structs são estruturas que declaram variáveis e seus tipos, que estão relacionadas ao contexto desse Struct. Por exemplo, um Struct de uma Pessoa irá definir dados relacionados com o contexto de uma Pessoa, como: Nome, Idade, CPF, Data de nascimento, vejamos um exemplo:

Veja que nesse caso, estamos usando o CPF no formato String, felizmente temos o crate validbr que nos fornece uma estrutura de dados para o CPF, junto com validação, vamos usa-la então.

Vejamos como ficou o novo código:

Nesse caso já temos uma boa estrutura para representar uma pessoa, agora vamos definir primeiro uma função para construir um valor do tipo Person, para isso iremos utilizar o keyword Impl, ele irá indicar que estamos implementando funções para um certo tipo. No Rust, as declarações e implementações de funções ficam fora do Struct, vejamos isso na prática:

Veja que nesse caso, a gente declara uma função new, mas devemos ficar atento que esta função está sendo declarada para o tipo Person, e não para o objeto. No Rust, toda vez que você precisar do objeto você precisa definir um parâmetro self na função, mas isso veremos mais para frente. Note também que temos uma função que praticamente vai se repetir para todas Structs que fizermos, até existem macros para não termos que repetir esse processo, mas não irei tratar disso nesse artigo.

Agora que temos uma Struct para representar uma pessoa, vamos definir algumas funções, uma delas será uma função para calcular a idade dessa pessoa. Para isso, continuaremos utilizando a biblioteca Chrono.

Apesar da biblioteca Chrono estar bastante avançada no quesito de tratamento de datas, ainda tem muito o que melhorar, como a introdução de contagem de dias do mês, bem como a implementação da estrutura Period que já temos a algum tempo em outras linguagens, como o Java, e como ainda não temos algo do tipo na biblioteca Chrono, iremos implementar algumas linhas a mais para a contagem de dias no ano.

Faremos isso, ao invés de utilizar um valor fixo, seja de 365 ou 366 dias, justamente para manter o tratamento mais genérico possível, não queremos assumir que uma certa estrutura de dados utilizando um calendário que não forçamos nem obrigamos, tenha um certo número de dias, isso porque, podemos no futuro (talvez muito distante) vir a utilizar um outro calendário a não ser o Gregoriano, e se não fizermos nenhum tipo de assunção, isso estará automaticamente implementado já.

Existem várias maneiras de contar a quantidade de dias em um ano, utilizando a biblioteca Chrono, um deles seria iterar em cada data possível neste ano, o problema aqui seria a complexidade do código, já que a quantidade de métodos que teríamos que utilizar iria poluir bastante o código, veja o exemplo abaixo calculando utilizando iteração:

Além disso, estamos utilizando muitos unwrap sem verificarmos a validade do resultado. No entanto, apesar de estarmos verificando cada nova data que é “criada” a cada iteração para saber se o ano continua o mesmo, esses valores não passam de números inteiros abstraídos em estruturas de dados, em outras palavras, não há diferença na performance geral dessa abordagem em comparação com a que iremos utilizar, que está logo abaixo:

Nessa abordagem, mantemos o código mais limpo, e deixamos claro que estamos calculando a quantidade de dias entre o primeiro dia do ano atual até o primeiro dia do próximo ano (exclusivamente).

Note que aqui forçamos um type casting utilizando a palavra as, mais para frente iremos aprender como fazer conversões de tipo de forma mais segura, por enquanto estamos assumindo que a idade de qualquer pessoa nunca irá passar do limite do tipo u8 que é 255, caso ocorra desse valor ultrapassar o programa irá fechar abruptamente com um erro de overflow.

Também note que utilizamos a palavra self para criar uma função associada ao objeto, ao invés de associada ao tipo. A palavra self deixa explicito que precisamos receber um valor do tipo da Struct para podermos operar, e também acessamos esse valor utilizando o self como fizemos com o campo age.

Outro recurso utilizado aqui é o Borrowing (&), que iremos ver mais a frente, nós utilizamos &self na declaração da nossa função, ao invés de apenas self, isso evita que nós nos tornemos donos do valor, e se nos tornarmos donos, quer dizer que iremos desalocar esse valor, mas não iremos fazer isso, por isso pegamos esse valor emprestado, assim podemos acessar seus dados, trabalhar eles, e depois devolver o valor principal.

Trait

Já vimos como é trabalhar com Structs, que não são nada menos que estruturas de dados. No entanto, as vezes queremos apenas descrever comportamentos, mas não implementar esses comportamentos em si, ai que entram os Traits.

Os Traits do Rust lembram bastante as interfaces de outras linguagens, como em C# e Java, porém o Rust não implementa Duck-typing como o Golang faz, ao mesmo tempo que, os Traits diferem em alguns aspectos das interfaces.

Digamos que agora queremos especificar o comportamento de uma estrutura capaz de armazenar um cadastro de pessoas, utilizando como chave principal o seu CPF, no entanto não nos importamos com a estrutura de dados que será utilizada para armazenar, se será um HashMap, um Set, ou um banco de dados, apenas queremos armazenar as pessoas e recuperar essas informações posteriormente, em outras linguagens faríamos uma espécie de repositório de pessoas utilizando uma interface, no entanto, em Rust, iremos utilizar Trait.

Primeiro de tudo, vamos criar um repositório genérico, que descreve o armazenamento de qualquer tipo de dado, não apenas dedicado ao tipo Person:

Aqui temos vários pontos para prestarmos atenção, primeiro deles, a utilização da palavra trait no inicio da declaração do Repository, outra delas é a utilização de tipos associados, quem já trabalhou com Swift já deve ter visto ou utilizado alguma vez, são tipos que estão associados diretamente ao seu tipo, e não a sua declaração isso melhora bastante a legibilidade do código, ao mesmo tempo que deixa a obrigação de definir qual o tipo para a implementação, e não para o desenvolvedor que irá utilizar.

Agora vamos implementar um repositório básico utilizando HashMap:

Note que para implementar nosso Trait, utilizamos a palavra impl junto com o for que indica qual tipo que está ganhando a implementação.

Agora já podemos utilizar nosso repositório:

Sim, parece que as coisas estão ficando complicadas, mas espere um pouco que eu vou explicar.

Option

É um Enum básico capaz de armazenar apenas dois tipos de valores: Some(E) e None, o None lembra bastante o null de outras linguagens, no entanto em Rust não existe null, o mecanismo para substituir isso é o Option, ele ajuda a tratar tanto a presença quanto ausência de valor. Utilizamos o Option sempre quando nosso retorno pode ser vazio ou ter algo, quando estamos falando de buscar um usuário em uma base de dados, pode ser que ele esteja lá, ou não, é ai que o Option entra, quando tiver, teremos um Some(Person), mas quando não tiver, teremos um None. Um mecanismo parecido com este é o Union Types do Ceylon, onde um valor pode ser apenas de um dos tipos especificados, em contraste com Intersection Types.

&mut

Em Rust, tudo é imutável, a menos que você especifique o contrário, e para dizer que algo pode ser mutado, você precisa especificar utilizando a palavra mut, ao fazer isso você permite que o tipo seja modificado onde você passar ele como tal, ou seja, você também precisa dar permissão as funções para que o valor seja mutado, no Rust você sempre sabe quando uma função pode modificar ou não o valor que você está passando. Nesse caso, o uso do & serve para indicar que estamos emprestando (olha o Borrowing ai novamente) um valor, em conjunto com o mut, formando o &mut dizemos que estamos emprestando um valor mutável.

Self::Key e Self::Item

O Self neste caso foi usado como um prefixo para acessar o tipo associado, então ele especifica que estamos acessando o tipo associado do nosso tipo, assim podemos utilizar esse tipo nas funções.

clone()

Utilizamos para criar um clone (não confundir com cópia) do nosso objeto, como nosso repositório obriga que passamos um valor no qual ele é dono, e não podemos fazer isso com o valor de Person já que ele já vai ter um dono (e não se pode ter dois donos do mesmo valor), precisamos clonar o CPF para que ele seja uma chave independente do valor de Person. Nós poderíamos utilizar sim o mesmo valor que já está em Person, mas nesse caso teríamos que trabalhar com lifetimes, que são um pouquinho avançadas para esse artigo inicial.

dyn

Para explicar o dyn primeiro devemos entender a natureza de Traits e Interfaces, ambos mecanismos introduzem tipagem dinâmica em linguagens de natureza estática, isso quer dizer que não sabemos exatamente qual código vai rodar até chegarmos nos tipos que os implementam e resolver suas funções. Tanto os Traits quanto Interfaces são tipos com resolução dinâmica, por isso precisamos utilizar o keyword dyn em Rust, para deixar claro ao programador que ele está utilizando um tipo com resolução dinâmica.

Box

Bom, agora vem onde as coisas começam a ficar interessantes, lembra que eu disse que Traits são tipos com resolução dinâmica? Então, isso significa que não sabemos o tamanho exato do valor que está implementando esse Trait, o que torna impossível armazenar esse valor na Stack, pois para estar na Stack precisamos saber seu tamanho antes, como o caso de Arrays, por exemplo.

O Box é utilizado para armazenar valores na Heap, sempre que tem um Box, quer dizer que o valor está na Heap, e seu ponteiro na Stack (e nesse caso, o ponteiro é o próprio Box), caso estivéssemos passando o próprio PersonRepository, nós saberíamos o tamanho dele e não precisaríamos armazenar o mesmo na Heap.

Conclusão

Nesse artigo aprendemos um pouco de Structs e caímos de paraquedas nos Traits, para já nos acostumarmos com os mecanismos da linguagem Rust. Também vimos alguns tipos como Option e Box, que vão ser bastante utilizados, e prepare-se, pois em Rust temos bastante wrappers, muito mais para frente iremos falar de Pin e Unpin quando estivermos trabalhando com código assíncrono, teremos também Mutex, Sync, Send, Arc, Rc e muitos outros.

--

--

Jonathan
Jonathan

Written by Jonathan

Developer, Bytecode Engineer, Cyber security analyst, Workaholic, Critic, Writer.

No responses yet