Aprendendo Rust do zero - Tipagem de dados 🎲

Aprendendo Rust do zero - Tipagem de dados 🎲

Tipos de dados que as variáveis em Rust podem ter.

Introdução 📝

Esse post faz parte de uma série que estou escrevendo chamada "Aprendendo Rust do zero" onde eu tento resumir o que acho ser importante baseado no livro oficial no site do Rust. Nesse aqui gostaria de abordar os tipos de dados. Atenção, escolhi manter alguns termos em inglês para facilitar a compreensão caso seja feito alguma busca na internet a partir desse post.

Sumário 📖

  1. Instalando Rust
  2. Hello, World!
  3. Hello, Cargo!
  4. Variáveis e Mutabilidade
  5. Tipos de dados - você está aqui
  6. Funções
  7. Comentários Simples
  8. Controle de Fluxo

Tipagem de dados 🎲

Cada valor em Rust é de um certo tipo de dado, que diz ao Rust como trabalhar com aquele dado. Vamos ver dois subgrupos de tipos de dados: scalar e compound.

Tenha em mente que Rust é uma linguagem de programação estáticamente tipada, que significa que o Rust precisa saber os tipos de todas as variáveis em tempo de compilação. O compilador pode também inferir qual o tipo nós estamos querendo usar baseado no valor e como o usamos. Em casos onde muitos tipos são possíveis nós precisamos declarar o tipo, como no exemplo a seguir:

let guess: u32 = "42".parse().expect("Not a number!");

Se o tipo não for declarado no exemplo acima, o Rust irá mostrar uma mensagem de erro dizendo que o compilador precisa de mais informação para saber qual tipo nós queremos usar, como podemos ver a seguir:

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^ consider giving `guess` a type

For more information about this error, try `rustc --explain E0282`.
error: could not compile `no_type_annotations` due to previous error

Veremos agora diferentes tipos que podem ser declarados.

Tipo Scalar 🧗‍♀️

Um tipo scalar representa um valor sozinho. O Rust tem quatro principais tipos dentro dessa categoria: integers, floating-point, booleans e characteres. Você provavelmente já viu esses tipos em outras linguagens de programação. Vamos ver como cada um desses funciona no Rust.

Tipo Integer

Um integer é um número sem a parte fracional (aqueles números após a virgula), também conhecido em português como inteiro. A seguir podemos ver as variações possíveis do tipo Integer:

image.png

Cada variação pode ser signed ou unsigned e tem um tamanho explícito. Os termos signed e unsigned informam quando o número é positivo ou negativo. Em outras palavras, se o número precisar ter um sinal na frente dele (signed) é negativo, caso o número possa ser representado sem um sinal na frente (unsigned) é posivito. É como se estivessemos escrevendo números no papel: se é negativo, precisamos colocar um tracinho na frente pra informar que é negativo, caso contrário, se for positivo é possível deduzir que ele é positivo sem colocar sinal nenhum, então não colocamos.

Cada variação signed pode armazenar números de -(2n-1) até 2n-1-1 inclusivo, onde n é o numéro de bits que a variante usa. Então a variação i8 pode armazenar números a partir de -(27) até 27-1, que seria igual a -128 até 127. Variações unsigned podem armazenar números de 0 a 2n-1, então a variação u8 pode armazenar a partir de 0 até 28-1, que é igual a 0 até 255.

Além desses, as variações isizee usize dependem do tipo de computador que seu programa está executando: 64 bits se você está em um computador com arquitetura de 64-bit e 32 bits se você está em um computador de arquitetura de 32-bit.

Você pode escrever literais numéricos integer em qualquer um dos formatos da tabela a seguir:

image.png

Note que literais numéricos que podem ter multiplos tipos numéricos permitem um sufixo, como 57u8, para designar o tipo. Literais numéricos podem também utilizar _ como um separador visual para fazer com que o número fique fácil de ler, como por exemplo 1_000, que teria o mesmo valor caso fosse especificado 1000.

Você pode estar se perguntando, qual variação de integer devo usar? Se você não estiver seguro de qual escolher, o Rust tem uma escolha padrão que pode ser uma boa para começar: tipos integer que não tem a variação definida, por padrão, sempre são i32. A situação principal em que você estaria utilizando isize ou usize é quando estiver indexando algum tipo de coleção.

Integer Overflow

Digamos que você tem uma variável do tipo u8 que pode armazenar valores dentre 0 e 255. Se você tentar mudar a variável para um valor fora desse intervalo, como por exemplo 256, irá ocorrer um integer overflow. Rust tem algumas regras envolvendo esse comportamento. Quando você está compilando em modo debug, o Rust inclui validações para integer overflow que fazem seu programa "entrar em pânico" em tempo de execução se esse comportamento ocorrer. Rust utiliza o termo "entrar em pânico" quando um programa finaliza com um erro.

Quando você compila em modo de produção, ou release, usando a flag --release, o Rust não inclui validações para integer overflow que causam pânico. Em vez disso, se um overflow ocorrer, o Rust executa um termo conhecido como two’s complement wrapping. De forma resumida, valores maiores que o valor máximo suportado pela variação, voltam ao valor mínimo suportado pela variação. No caso de um u8, o valor 256 se torna 0, o valor 257 se torna 1 e assim por diante. O programa não vai "entrar em pânico", mas a variável irá ter um valor que provavelmente não é o que você está esperando que seja. Confiar no comportamento de integer overflow, pode ser considerado um erro.

Para explicitamente gerenciar a possibilidade de overflow, você pode usar as seguints famílias de métodos que a instalação padrão do Rust provê para tipos númericos primitivos:

  • Circundar todos os modos com os métodos wrapping_*, como por exemplo wrapping_add
  • Retornar o valor None se houver um overflow com os métodos checked_*
  • Retornar o valor e um boolean indicando se houve ou não um overflow com os métodos overflowing_*
  • Sature nos valores máximos ou mínimos com os métodos saturating_*

Tipo Floating-Point

Rust também tem dois tipos primitivos para números de ponto flutuante, ou floating-point, que são números com casas decimais. Eles são f32 e f64, que tem tamanhos de 32 bits e 64 bits respectivamente. O tipo padrão é f64 pois nas CPU's modernas e atuais, é utilizado quase a mesma velocidade que f32, porém, é capaz de mais unidades de precisão. A seguir um exemplo mostrando números de ponto flutuante na prática:

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

Números de ponto flutuante são representados de acordo com o padrão IEEE-754. O tipo f32 é um float de única precisão, e o tipo f64 tem dupla precisão.

Operações Numéricas

Rust suporta as operações matemáticas básicas que você esperaria para todos os tipos numéricos: adição, subtração, multiplicação, divisão e resto de divisão. Divisão de inteiros arredonda para baixo para o inteiro mais próximo. O exemplo a seguir mostra cada operação númerica utilizando variáveis declaradas com let:

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let floored = 2 / 3; // Results in 0

    // remainder
    let remainder = 43 % 5;
}

Cada expressão nesse código utiliza uma operação matemática e retorna um valor único que então é atribuido à variável.

O tipo Boolean

Como na maioria das linguagens de programação, o tipo Boolean em Rust tem dois possíveis valores: true e false. Booleans ocupam um byte em tamanho. O tipo Boolean no Rust é especificado utilizando bool, como podemos ver a seguir:

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

O principal uso de valores Boolean é através de condicionais, como em uma expressão if.

O tipo Caractere

Até aqui vimos somente tipos numéricos, porém o Rust suporta letras também. O tipo char é o tipo mais primitivo quando falamos em caracteres do alfabeto, e o exemplo a seguir mostra uma forma de o utilizar (perceba que literais do tipo char são especificados com aspas simples, ao contrário de literais do tipo string, que usam aspas duplas):

fn main() {
    let c = 'z';
    let z = 'ℤ';
    let heart_eyed_cat = '😻';
}

O tipo char em Rust tem o tamanho de quatro bytes e representa um Unicode Scalar Value, que signifca que ele pode representar muito mais que simplesmente ASCII. Letras com acento, caracteres Japoneses, Chineses e Coreanos, emoji e caracteres de espaço com zero de largura são todos valores char válidos em Rust. Unicode Scalar Value variam em um intervalo de U+0000 até U+D7FF e de U+E000 até U+10FFFF inclusive. Porém, um caractere não é realmente um conceito em Unicode, então a sua intuição humana para o que um "caractere" é talvez não encaixe com o que um char é em Rust.

Tipos Compound 🔋

Tipos compound podem agrupar multiplos valores em um tipo. Rust tem dois tipos compound primitivos: tuples e arrays.

O tipo Tuple

Um tuple é uma forma genérica de agrupar um número de valores com variados tipos em um tipo composto só. Tuples tem um tamanho fixo: uma vez declarados, eles não podem crescer ou diminuir de tamanho.

Nós podemos criar um tuple escrevendo uma lista de valores separados por vírgula dentro de parênteses. Cada posição no tuple tem um tipo, e os tipos de diferentes valores no tuple não tem que ser iguais, no exemplo a seguir foram adicionados os tipos de cada valor, isso é opicional:

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

A variável tup recebe todo o conteúdo do tuple, pois o tuple é considerado um único elemento composto. Para obter valores individuais de um tuple, nós podemos usar desestruturação, como a seguir:

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {}", y);
}

Esse código primeiro cria o tuple e atribuí esse valor à variável tup. É então utilizado então desestruturação para separar esse valor composto em três únicas variáveis. Por último o código imprime o valor de y, que é igual a 6.4.

Além da desestruturação, é possível acessar os elementos de um tuple diretamente usando um ponto . seguido pelo índice do valor que nós queremos acessar, como podemos ver a seguir:

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

Esse código cria um tuple, x, e então faz novas variáveis para cada elemento usando seus respectivos índices. Assim com na maioria das linguagens de programação, o primeiro índice é 0.

Um tuple sem nenhum valor, (), é um tipo especial que tem somente um valor, também escrito (). Esse tipo é chamado de unit type e o seu valor é chamado de unit value. Expressões implicitamente retornam esse unit value se elas não retornarem nenhum outro valor.

O tipo Array

Uma outra forma de ter uma coleção de múltiplos valores é com um array. Diferente do tuple, cada elemento de um array deve ter o mesmo tipo. Arrays em Rust são diferentes de arrays em outras linguagens de programação pois arrays em Rust tem um tamanho fixo, como tuples.

Em Rust, os valores que fazem parte do array são escritos como uma lista separados por vírgula dentro de colchetes:

fn main() {
    let a = [1, 2, 3, 4, 5];
}

Arrays são uteis quando você quer garantir que sempre terá um número fixo de elementos. Um array não é flexível como o tipo vector. Um vector é um tipo similar de coleção provido pela instalação padrão que pode aumentar ou diminuir de tamanho. Se você não tiver certeza se deve usar array ou vector, você provavelmente deveria usar um vector.

Um exemplo onde você deveria usar um array e não um vector é em um programa que precisa saber o nome dos meses do ano. É bem improvável que esse tipo de programa precisará adicionar ou remover meses, então você pode usar um array pois sempre haverão 12 elementos:

let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];

Para definir o tipo de um array é necessario utilizar colchetes e colocar dentro dos colchetes o tipo de todos os elementos, um ponto e vírgula e o número de elementos no array, como a seguir:

let a: [i32; 5] = [1, 2, 3, 4, 5];

Nesse exemplo, i32 é o tipo de cada elemento. Depois do ponto e vírgula, o número 5 indica que o array contém cinco elementos.

Existe uma forma alternativa de iniciar um array. Se você quer criar um array que contenha o mesmo valor em cada elemento, você pode especificar o valor inicial, seguido por um ponto e vírgula e o tamanho do array dentro de colchetes, como mostrado a seguir:

let a = [3; 5];

O array chamado a terá 5 elementos que irão todos ter o mesmo valor 3 inicialmente. Isso é o mesmo que escrever let a = [3, 3, 3, 3, 3];, porém de uma forma mais concisa.

Acessando elementos do array

Um array é um pedaço único da memória de um tamanho fixo e conhecido que pode ser alocado na stack. Você pode acessar os elementos de um array usando os índices, como a seguir:

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

Nesse exemplo, a variável chamada first obterá o valor 1, pois é o valor presente na posição [0]. A variável second obterá o valor 2da posição [1]no array.

Acessando índices inválidos no array

Caso você tente acessar um índice maior ou igual que o tamanho definido para o array, o Rust irá "entrar em pânico" como mencionado anteriormente. Isso demonstra um princípio de segurança do Rust em ação. Na maioria de linguagens de programação de baixo-nível, esse tipo de comportamento não acontece e quando um índice incorreto é utilizado, memória inválida pode ser acessada. Rust protege você contra esse tipo de erro finalizando imediatamente a execução do programa em vez de permitir o acesso à memória e continuar a execução.

No próximo post vamos aprender sobre funções.

Referência 📚

Para escrever esse post e os possíveis próximos que virão, estou utilizando o livro online recomendado no site oficial do Rust, que você pode encontrar em doc.rust-lang.org/book/title-page.html.