Rust 0x02 = Variáveis e Tipos Primitivos;

Gil Mendes
10 min readJan 8, 2017

--

Neste terceiro artigo da série Rust vamos abordar dois assuntos, variáveis e tipos primitivos. Assumindo que todos que estão a ler estes artigos são programadores ou no minimo, têm noções de programação, não vou abordar em grande detalhe, conceitos básicos da programação.

Variáveis

Em programação, uma variável permite guardar temporariamente um determinado valor, valor que pode ser alterado.

Em Rust, a palavra reservada let permite fazer a associação de um valor a uma variável (também conhecido como variable bindings).

fn main() {
let x = 5;
}

Nos próximos exemplos vou deixar de colocar o fn main() {, pois é chato estar sempre a repetir, logo, ficará de fora no futuro. Se está a seguir a série, tenha em atenção que tem que editar o corpo da sua função main. Contrário irá dar erro.

Patterns

Na generalidade das linguagens, uma atribuição a uma variável é isso mesmo uma variável, mas em Rust é um pouco diferentes é mais parecido com uma ligação. A parte esquerda da declaração do let diz respeito a um patter, não a um nome de variável. Isto quer dizer que podemos fazer coisas tipo:

let (x, y) = (1, 2);

Após a avaliação esta declaração, x será um (1) e y será dois (2). Os patterns são realmente poderosos e por isso mesmo terão um artigo apenas dedicado a eles. Por agora, não existe necessidade de usarmos esta funcionalidade, por isso vamos continuar a usar a boa e velha forma de declarar variáveis.

Anotação de Tipos

O Rust é uma linguagem estaticamente tirada, isso quer dizer que nós especificamos o tipo e depois ele é validado na altura da compilação do programa. Agora vocês perguntam, Mas como é que o nosso primeiro programa compilou? Bem…, o Rust tem uma coisa chamada type inference (ou em português, inferência de tipos), o compilador consegue perceber o tipo que estamos a atribuir à variável sem a necessidade de o indicar.

Atenção! Se mais tarde tentar atribuir uma variável de um tipo diferente o compilador irá mostrar um erro. Visto que o Rust é uma linguagem fortemente tipada, não é possível alterar o tipo de uma variável.

A declaração do tipo vem após dois pontos (:), tal como pode ser visto abaixo:

let x: i32 = 5;

Traduzindo esta declaração, seria algo como: “x é uma ligação com o tipo i32 e com o valor de cinco”.

Neste exemplo escolhi representar x como sendo um inteiro de 32-bit com sinal. Em Rust existe inúmeros tipos primitivos inteiros, que serão abordados mais adiante neste artigo. Por agora o que interessa saber é que os começados por i dizem respeito a um inteiro com sinal e os começados com u dizem respeito a um inteiro sem sinal. Existe ainda uma versão de cada um em 8, 16, 32 e 64 bits.

Mutabilidade

Por defeito as variáveis são imutáveis. Isso quer dizer que não é possível alterar o seu valor. O exemplo abaixo não vai compilar e irá lanchar um erro:

let x = 5;
x = 10;

E o erro será:

error[E0384]: re-assignment of immutable variable `x`
--> src/main.rs:3:5
|
2 | let x = 5;
| - first assignment to `x`
3 | x = 10;
| ^^^^^^ re-assignment of immutable variable

Se quiser declarar uma variável que seja possível alterar o seu valor, você tem que usar a palavra reservada mut:

let mut x = 5;
x = 10;

Não existe apenas uma razão para as variáveis serem imutáveis por defeito, de uma forma simples é por causa de questões relacionadas com a segurança. Esse que é um dos principais objetivos do Rust. Se você se esquecer de escrever mut o compilador irá perceber isso e irá informa-lo que está a alterar uma variável que possivelmente não queria. Se as variáveis fossem mutáveis por defeito, o compilador não seria capaz de fazer este tipo de deteção, logo, erros de codificação iriam ser mais frequentes.

Inicialização

A inicialização é mais um ponto em que o Rust difere das outras linguagens. As variáveis têm obrigatoriamente que ser inicializadas, com um valor para ser usadas. Vamos alterar o nosso ficheiro src/main.rs com o conteúdo que vê abaixo e de seguida vamos executa-lo:

fn main() {
let x: i32;

println!("Hello, world!");
}

Neste caso, uma vez que não estamos a fazer uso da variável, o nosso programa irá compilar, mas é mostrado um warning na consola:

warning: unused variable: `x`, #[warn(unused_variables)] on by default
--> src/main.rs:2:9
|
2 | let x: i32;
| ^

O caso muda de figura se tentar usar a variável. Vamos alterar o nosso programa para fazer uso da variável declarada.

fn main() {
let x: i32;

println!("O valor de x é: {}", x);
}

Compilamos e agora deparame-nos com um erro:

error[E0381]: use of possibly uninitialized variable: `x`
--> src/main.rs:4:36
|
4 | println!("O valor de x é: {}", x);
| ^ use of possibly uninitialized `x`

O Rust irá informar que está a fazer uso de uma variável não inicializada. Mais uma vez, um bom mecanismo para prevenir problemas no futuro. Uma coisa é escrever um programa com meia dúzia de linhas, outra, quando passamos das 300, fica mais complicada gerir, por isso toda a ajuda é bem vinda.

Vamos falar de algo novo que introduzi no exemplo anterior, o println!. Se você incluir duas chavetas ({}, também conhecidas como moustaches) na string para ser impressa, o Rust irá interpretar isto como um pedido de interpolação e irá tentar imprimir o valor de x de uma forma legível por humanos.

Existe um grande conjunto de opções para passar para o println!, que pode ver em maior detalhe neste link. Por agora vamos-nos focar na impressão por defeito, inteiros não são assim tão complicados de imprimir.

Scope e Shadowing

Voltando à declaração de variáveis, estas têm um scope (âmbito), a sua existência é restrita a esse scope, o bloco onde elas vivem. Um bloco é um conjunto de declarações envolvidas entre chavetas ( { e } ). A definição de funções também são blocos! No exemplo abaixo podemos ver definidas duas variáveis x e y, que vivem em diferentes blocos. A variável x pode ser acedida dentro do bloco da função fn main() {}, enquanto y apenas pode ser acedida dentro do bloco aninhado.

fn main() {
let x: i32 = 20;
{
let y: i32 = 3;
println!("O valor de x é {} e o valor de y é {}", x, y);
}

// isto não vai funcionar
println!("O valor de x é {} e o valor de y é {}", x, y);
}

O primeiro println! iria imprimir “O valor de x é 20 e o valor de y é 3”, mas, este exemplo não irá compilar com sucesso, porque o segundo println! não consegue aceder à variável y. Ao compilar irá ver um erro semelhante ao seguinte:

error[E0425]: unresolved name `y`
--> src/main.rs:9:56
|
9 | println!("O valor de x é {} e o valor de y é {}", x, y);
| ^ did you mean `x`?

error: aborting due to previous error

Adicionalmente as variáveis podem ser “sombreadas”(sobrepostas). Isto significa que uma declaração de uma variável com o mesmo nome de uma anterior, no mesmo scope, irá substituir a anterior.

let x: i32 = 20;
{
println!("{}", x); // imprime "20"
let x = 10;
println!("{}", x); // imprime "10"
}
println!("{}", x); // imprime "20"
let x = 60;
println!("{}", x); // imprime "60"

O sombreamento e a mutabilidade parecem dois lados da mesma moeda, mas são duas coisas completamente distintas. Por um lado, sobreposição permite redefinir uma variável com o mesmo nome e com um tipo diferente. Mas não é tudo, também é possível alterar a mutabilidade dessa variável.

let mut x: i32 = 7;
x = 40;
let x = x; // agora x é imutavel e possui o valor 40

let y = 5;
let y = "Agora eu sou uma string!";

Tipos Primitivos

A linguagem Rust contem um grande numero de tipos considerados “primitivos”. Isto significa que eles estão embebidos na própria linguagem. O Rust é estruturado de uma forma que a standard library também fornece um grande numero de tipos úteis construídos no topo dos tipos primitivos. Abaixo estão todos os tipos considerados primitivos.

Booleans

Em Rust o tipo boleano está embebido na linguagem e chama-se bool. Este pode ter dois valores possíveis, true e false.

let x = true;

let y: bool = false;

Um uso comum dos tipos boleanos é nas condições if, mas lá chegaremos.

Documentação oficial do tipo bool.

Char

O tipo char representa um único caractere Unicode. É possível criar um único caractere usando plicas (‘).

let x = 'x';
let world = '🌍';

Ao contrario de outras linguagens, o Rust ocupa 4 bytes para armazenar um char e não apenas um.

Documentação oficial do tipo char.

Tipos Numéricos

O Rust contem uma grande variedade de tipos numéricos, estes podem ser divididos em algumas categorias: com sinal ou sem sinal, fixo ou variável com vírgula flutuante ou inteiro. Estes tipos consistem em duas partes: categoria e tamanho. Por exemplo u16 é um tipo sem sinal de dezasseis bits de comprimento. Mais bits, permitem guardar números maiores.

Se na declaração da variável não for especificado o tipo da mesma, uma tipo será inferido com base na declaração da mesma. Pode ver isso no exemplo abaixo:

let x = 30; // x é do tipo i32

let y = 1.0; // y é do tipo f64

Aqui fica uma lista dos diferentes tipos, com os links para a respetiva documentação:

Segundo um commit que vi, numa das próximas versões pode ser adicionado suporte para numéricos de 128 bits.

Tipos de Tamanho Fixo

Tipos de tamanho fixo têm um numero especifico de bits na sua representação. Atualmente, este numero pode ser 8, 16, 32 e 64. Assim sendo, um u32 é não sinalizado e tem um tamanho fixo de 32-bit e um i64 é sinalizado e tem um tamanho fixo de um inteiro de 64-bit.

Tipos de Tamanho Variável

Em Rust existem alguns tipos que possuem um tamanho dinâmico de acordo com a plataforma em que estes estão a correr. O seu tamanho é definido pela arquitetura da maquina. Estão disponíveis os dois seguintes tipos, isize e usize, para a representação de numéricos com e sem sinal respetivamente.

Arrays

Como em muitas outras linguagens de programação, o Rust possui tipos específicos para a representação de sequências de coisas. O tipo mais básico é o array, este possui um tamanho fixo de posições de elementos do mesmo tipo. Por defeito arrays são imutáveis.

let a = [30, 40, 47];
let mut m = [39, 18, 20];

Os arrays tem um tipo [T; N]. Mais à frente iremos falar deste T, quando abordamos os tipos genéricos. Já o N trata-se de uma constante que permite definir o tamanho do array.

Existe uma forma curta de inicializar cada elemento de um array com o mesmo valor. Neste exemplo cada elemento do array a é inicializado a 0:

let a = [0; 20];

Documentação oficial do tipo array.

Slices

Um slice é uma referencia para uma outra estrutura de dados. Estes são úteis para permitir um acesso seguro e eficiente a uma porção de um array sem o copiar. Por exemplo, você pode apenas querer a referência de um linha de um ficheiro em memória. Normalmente, um slice não é criado diretamente, mas sim através de uma variável. Um slice tem um tamanho definido e pode ser mutável ou imutável.

Internamente, um slice é representado como um apontador para o inicio dos dados e com um comprimento.

Sintaxe de um Slice

Você pode usar um combo de & e [] para criar um slice a partir de diferentes coisas. O & indica que queremos uma referência (que também vamos falar em detalhe no futuro) e o [] permite definir o intervalo dos dados:

let a = [0, 1, 2, 3, 4];
let complete = &a[..]; // Um slice que contem todos os elementos de a
let middle = &a[1..4]; // Um splice de a com apenas os elementos 1, 2 e 3

O slices são do tipo &[T].

Tuples

Um tuplo é uma lista ordenada de tamanho fixo. Esta lista, ao contrario dos arrays podem contem elementos de tipos diferentes.

let x = (1, "olá");

No caso de ser necessário usar anotação de tipos, pode fazer-lo usando a sintaxe abaixo:

let x: (i32, &str) = (1, "olá");

Você pode atribuir um tuplo a outro, desde que este seja constituído pelos mesmos tipos de dados e números de elementos.

let mut x = (1, 2);
let y = (2, 3);

x = y;

Você pode aceder aos elementos de um tuplo através de destructuring:

let (x, y, z) = (1, 2, 3);

println!("o valor de x é {}", x);

É preciso ter em atenção as seguintes situações:

(0,); // isto é um tuplo de um elemento
(0); // isto é um zero entre parênteses

Indices e Tuples

Os tupulos também possuem uma sintaxe para aceder aos seus elementos através dos indices:

let tuple = (1, 2, 3);

let x = tuple.0;
let y = tuple.1;
let z = tuple.2;

println!("o valor de x é {}", x);

Tal como os indices dos arrays, estes também começam em zero, mas ao contrario dos arrays, para aceder aos indicies usamos um ponto (.) e não parênteses retos ([]).

Documentação oficial do tipo tuple.

Funções

As funções também têm um tipo. Abaixo encontra-se um exemplo de como usar:

fn foo(x: i32) -> i32 { x }

let uma_funcao: fn(i32) -> i32 = foo;

Neste caso, uma_funcao é um apontador para a função foo que recebe um i32 e retorna um i32.

Como pode ver Rust difere um pouco das outras linguagens em alguns aspetos. Poderá levar algum tempo para se adaptar, mas assim que o fizer, duvido que queira programar em outra linguagem.

Por hoje é tudo, no proximo artigo vamos às funções.

Fiquem em paz e boas codificações! 🙂

--

--