Uma coisa muito importante a se resaltar em Rust é que suas variáveis são imutáveis por default. Devemos sempre lembrar que Rust é uma linguagem estática - que garante maior segurnaça e confiabilidade aos seus códigos. Entretante, No entanto, você ainda tem a opção de tornar suas variáveis mutáveis. Vamos explorar como e por que o Rust o incentiva a favorecer a imutabilidade.
Abra um arquivo e coloque o seguinte código abaixo:
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
Você verá que a saida do código será semelhante a imagem abaixo:
Este exemplo mostra como o compilador o ajuda a encontrar erros em seus programas. Os erros do compilador podem ser frustrantes, mas na verdade eles apenas significam que seu programa ainda não está fazendo com segurança o que você deseja.
A mensagem de erro indica que a causa do erro é que você não pode atribuir duas vezes à variável imutável x
, porque você tentou atribuir um segundo valor à variável imutável x.
É importante que recebamos erros em tempo de compilação quando tentamos alterar um valor designado como imutável, pois essa mesma situação pode levar a erros. Se uma parte de nosso código opera na suposição de que um valor nunca mudará e outra parte de nosso código altera esse valor, é possível que a primeira parte do código não faça o que foi projetada para fazer. A causa desse tipo de bug pode ser difícil de rastrear após o fato, especialmente quando a segunda parte do código altera o valor apenas algumas vezes. O compilador Rust garante que quando você declara um valor não mudará, ele realmente não mudará, então você não precisa acompanhá-lo por conta própria. Seu código é, portanto, mais fácil de raciocinar.
Mas a mutabilidade pode ser muito útil e pode tornar o código mais conveniente para escrever. Embora as variáveis sejam imutáveis por padrão, você pode torná-las mutáveis adicionando mut
na frente do nome da variável como você fez no Capítulo 2. Adicionar mut
também transmite a intenção para futuros leitores do código, indicando que outras partes do código serão alteradas o valor desta variável.
Quando mudamos o código muda para:
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
O código compila corretamente. Veja a imagem abaixo:
Em última análise, decidir usar ou não a mutabilidade depende de você e depende do que você acha que é mais claro nessa situação específica.
Assim como as variáveis imutáveis, as constantes são valores vinculados a um nome e não podem ser alterados, mas há algumas diferenças entre constantes e variáveis. Você pode declarar constantes usando a palavra-chave const
em vez da palavra-chave let
, e o tipo do valor deve ser anotado. Ademais, outra diferença é que as constantes podem ser definidas apenas para uma expressão constante, não o resultado de um valor que só pode ser calculado em tempo de execução.
Varemos um exemplo abaixo:
const UMA_HORA_EM_SEGUNDOS: u32 = 60 * 60;
As constantes são válidas durante todo o tempo em que um programa é executado, dentro do escopo em que foram declarados. Essa propriedade torna as constantes úteis para valores em seu domínio de aplicativo que várias partes do programa podem precisar saber, como o número máximo de pontos que qualquer jogador de um jogo é permitido ganhar ou a velocidade da luz.
O processo de sombreamento é basicamente quando uma variável é sobreposta. Em outras palavras, é quando você declara uma nova variável com o mesmo nome de uma variável anterior. Dizemos que a primeira variável foi sombreada pela segunda - o que significa que a segunda variável é o que o compilador verá quando você usar o nome da variável.
Podemos sombrear uma variável usando o nome da mesma variável e repetindo o uso do let
. Fizemos algo parecido nos exemplos acima, mas iremos nos aprofundar nesse tópido.
Veja o exemplo de Shadowing abaixo:
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
println!("The value of x is: {x}");
}
Este programa primeiro vincula x
a um valor de 5
. Em seguida, ele cria uma nova variável x
repetindo let x =
, pegando o valor original e adicionando 1
para que o valor de x
seja 6
. Então, dentro de um escopo interno criado com o curly colchetes, a terceira instrução let
também sombreia x
e cria uma nova variável, multiplicando o valor anterior por 2
para dar a x
um valor de 12
. Quando esse escopo termina, o sombreamento interno termina e x
volta a ser 6
.
A diferença entre mut e shadowing é que, como estamos criando efetivamente uma nova variável quando usamos a palavra-chave let novamente, podemos alterar o tipo do valor, mas reutilizar o mesmo nome. Além disso, se acidentalmente tentarmos reatribuir a esta variável sem usar o let, será exibido uma mensagem de erro.
Veja o exemplos abaixo:
fn main() {
let spaces = " ";
let spaces = spaces.len();
println!("O valor dos espaços é: {spaces}");
}
Com isso, podemos mudar o tipo da variável.
Isso já não seria possível utilizando-se mut
.
Cada valor no Rust é de um determinado tipo de dados, que informa ao Rust que tipo de dados está sendo especificado para que ele saiba como trabalhar com esses dados. Veremos dois subconjuntos de tipos de dados: escalar e composto. Tenha em mente que Rust é uma linguagem de tipagem estática, o que significa que ela deve conhecer os tipos de todas as variáveis em tempo de compilação.
Veja um exemplo:
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}
Se não adicionarmos a anotação do tipo :u32
acima, o Rust exibirá o seguinte erro, o que significa que o compilador precisa de mais informações nossas para saber qual tipo queremos usar:
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 as notações para cada tipo de dado:
Um tipo escalar representa um único valor. Rust tem quatro tipos escalares primários: inteiros, números de ponto flutuante, booleanos e caracteres.
INTEIROS
Um inteiro é um número sem um componente fracionário. Existem dois tipos de inteiros no Rust. Signed e Unsigned refere-se à possibilidade do número ser negativo ou positivo - em outras palavras, se o número precisa de um sinal com ele (signed) ou se sempre for positivo pode ser representado sem um sinal (unsigned). Devido a essa característica, é utilizado i
para representar singed e u
para unsigned.
Cada variante com sinal pode armazenar números de n
o número de bits que varia de acordo com o uso. Então, um i8
pode armazenar números de u8
pode armazenar números de
Podemos pensar em termos das tabelas abaixo:
Tamanho | Signed | Intervalo de Valores |
---|---|---|
8-bit | i8 | -128 à 127 |
16-bit | i16 | -32768 à 32767 |
32-bit | i32 | -2147483648 à 2147483647 |
64-bit | i64 | -9223372036854775808 à 9223372036854775807 |
128-bit | i128 | -170141183460469231731687303715884105728 à 170141183460469231731687303715884105727 |
arch | isize | isize |
Para Unsigned
Tamanho | Signed | Intervalo de Valores |
---|---|---|
8-bit | u8 | 0 à 255 |
16-bit | u16 | 0 à 65535 |
32-bit | u32 | 0 à 4294967295 |
64-bit | u64 | 0 à 18446744073709551615 |
128-bit | u128 | 0 à 340282366920938463463374607431768211455 |
arch | usize | usize |
Além disso, os tipos isize e usize dependem do computador em que seu programa está rodando: 64 bits se estiver em uma arquitetura de 64-bit e 32 bits se sua arquitetura for 32-bit. Um caso mais raro é para processadores 128 bits.
Os inteiros podem, alternativamente, ser expressos usando notação hexadecimal, octal ou binária usando estes prefixos respectivamente: 0x
, 0o
ou 0b
. Em outras palavras, também podemos utilizar um outro sistema de numeração, como octal ou hexadecimal:
Números literais | Exemplo |
---|---|
Decimal | 98_222 |
Hexadecimal | 0xff |
Octal | 0o77 |
Binário | 0b1111_0000 |
Byte (u8 apenas) | b'A' |
As escolhas padrões do Rust geralmente são boas, e por padrão os inteiros são do tipo i32
: Esse tipo geralmente é o mais rápido, até em sistemas de 64-bit. A principal situação em que você usuaria isize
ou usize
é indexar algum tipo de coleção.
PONTO FLUTUANTE
Rust também tem dois tipos primitivos para números de ponto flutuante, que são números com casas decimais. Os pontos flutuantes do Rust são f32
e f64
, que têm respectivamente os tamanhos de 32 e 64 bits.
Veja o seguinte exemplo:
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
Números em ponto flutuante são representados de acordo com o padrão IEEE-754. O tipo f32 é de precisão simples e f64 tem precisão dupla.
BOOLEANOS
Como em diversas linguagens de programação, o tipo Booleano em Rust possue dois valores possíveis: true
e false
.
Um exemplo de código booleano:
fn main() {
let t = true;
let f: bool = false; // com tipo explícito
}
CARACTERES
Até agora trabalhamos apenas com números, mas Rust também suporta letras. O char
é o tipo mais primitivo da linguaguem e o seguinte código mostra uma forma de utilizá-lo. (Observe que o char é específicado com aspas simples, é o oposto de strings, que usa aspas duplas.)
OBS: O tipo char
representa um valor unicode, o que quer dizer que você pode armazenar muito mais que apenas ASCII. Os valores Unicode vão de U+0000
até U+D7FF
e U+E000
até U+10FFFF
incluso.
Veja o exemplo abaixo:
fn main() {
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻';
}
Tipos compostos podem agrupar vários valores em um único tipo. Rust tem dois tipos primitivos compostos: tuplas e vetores.
TUPLAS
Em geral, uma tupla é uma coleção não ordenada de itens. Criamos uma tupla escrevendo uma lista de valores separados por vírgula dentro de parênteses. Cada posição da tupla tem um tipo e os tipos dos elementos da tupla não necessitam serem iguais.
Um exemplo:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
variável tup
liga-se a tupla, porque uma tupla é considerada um único elemento composto. Além de desestruturar através da correspondência de padrões, podemos acessar diretamente um elemento da tupla usando um ponto (.)
como índice do valor que queremos acessar. Por exemplo:
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let quinhentos = x.0;
let seis_ponto_quatro = x.1;
let um = x.2;
}
MATRIZES
Em geral, mma matriz é uma coleção ordenada de itens. Em outras palavras, uma outra maneira de ter uma coleção de vários valores é uma matriz. Diferentemente de uma tupla, todos os elementos de uma matriz devem ser do mesmo tipo.
Em Rust, os valores que entram numa matriz são escritos em uma lista separados por vírgulas dentro de colchetes:
fn main() {
let a = [1, 2, 3, 4, 5];
}
Matrizes são úteis quando você deseja que seus dados sejam alocados em pilha do que no heap (Discutiremos posteriormente).
Um exemplo de quando você poderia necessitar usar uma matriz no lugar de um vetor é um programa em que você precisa saber o nome dos meses do ano. É improvável que tal programa deseje adicionar ou remover meses, então você pode usar uma matriz porque você sabe que sempre conterá 12 itens:
let meses = ["Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho",
"Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"];
Para acessar um determinado item em uma matriz, utilize um índice entre parenteses. Veja o exemplo abaixo:
fn main() {
let meses = ["Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho",
"Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"];
let primeiro = meses[0];
let segundo = meses[1];
}
Em Rust, algo importante a ser ressaltado é que podemos realizar a mudança de um tipo em outro tipo. Usualmente, o compilado Rust não permite que diferentes tipos realizem operações. Em outras palavras, se desejarmos fazer uma divisão de um tipo i32
por outro u8
, como exemplo, o compilador não permitirá. Por isso, podemos utilizar o as
(como, em português) para tratar essas transformações.
let a: u8 = 10;
let b: i32 = 100;
print!("{}", b/a); // Não é possível fazer a divisão
Agora utilizando o as
:
fn main(){
let a: u8 = 10;
let b: i32 = 100;
print!("{}", b/a as i32); // Possivel usar a divisão
// Rust lê o resultado de b/a como sendo uma variável i32
}
Para entender um pouco mais sobre o tema, veja o vídeo abaixo:
➡️ AVANÇAR PARA O PRÓXIMO HANDS-ON? ➡️Clique Aqui
REFERÊNCIAS BIBLIOGRÁFICAS
[1] - Variables and Mutability. The Rust Programming Language - doc.rust-lang.org. Disponível em: https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html. Acesso em 10/09/2022.
[2] - Data Types. The Rust Programming Language - doc.rust-lang.org. Disponível em: https://doc.rust-lang.org/book/ch03-02-data-types.html. Acesso em 10/09/2022.