Rust 0x06 = Match, Patterns & Strings;

Gil Mendes
10 min readFeb 6, 2017

--

Olá mais uma vez. Se ainda me está a acompanhar muitos parabéns, aos outros, não os condeno, depois do ultimo artigo é normal que alguns desistam ou levem mais um pouco a digerir tanta informação.

Bom, para acalmar um pouco, este artigo será mais soft. Vamos finalmente falar e explicar o que é o match e o que são Patterns em Rust. Se tiver atento notará que já fizemos uso do match, mas só agora é que vamos introduzir o conceito de forma apropriada. Por isso, vamos falar sobre Strings de uma forma um pouco mais aprofundada, mas nada muito complexo ou extenso.

Match

Por vezes, um simples if/else não é suficiente, uma vez que podem haver imensas opções possíveis, além disso, as condições podem-se tornar um pouco complexas. Para estes casos, o Rust, possui a palavra reservada match, esta permite-nos substituir uma expressão if/else e agrupa-los de uma forma mais poderosa, como pode ver abaixo:

let x = 5;

match x {
1 => println!("Um"),
2 => println!("Dois"),
3 => println!("Três"),
4 => println!("Quatro"),
5 => println!("Cinco"),
_ => println!("Outro"),
}

Como pode ver, o match recebe uma expressão e entre chavetas {}, onde colocamos as possíveis correspondências com o valor da expressão. Cada braço, digamos assim, é descrito na forma valor => expressão. Quando o valor corresponde, a expressão equivalente a esse valor é executada. É chamado match porque advém do "pattern matching", em que o match é uma implementação deste. Sobre padrões (patterns) irei falar mais à frente.

Uma das vantagens de usar o match é que ele obriga a cobrir todas as possibilidades, caso não o façamos o compilador irá dar uma erro. Basta remover o underscore (_) no exemplo acima:

error[E0004]: non-exhaustive patterns: `_` not covered
--> src/main.rs:4:11
|
4 | match x {
| ^ pattern `_` not covered

O Rust está-nos a dizer que nos esquecemos de alguns valores. O compilador consegue inferir que x é do tipo i32 em que os seus valores podem variar entre -2,147,483,648 e 2,147,483,647. O _ atua como um "apanha tudo", uma vez que irá apanhar todos os valores que não correspondem com nenhum outro braço.

Outro uso muito útil do match é a possibilidade de atribuir diretamente o valor da expressão do braço escolhido a uma variável, tal como pode ver no exemplo abaixo:

let x = 5;

let numero = match x {
1 => "Um",
2 => "Dois",
3 => "Três",
4 => "Quatro",
5 => "Cinco",
_ => "Outro"
};

É realmente uma forma muito útil de converter um tipo para um outro. Neste caso permitiu converter um inteiro para uma String.

Patterns

Os patterns são bastantes comuns no Rust. Podemos vê-los na definição de variáveis, nos match (como vimos anteriormente), mas também em muitas outras situações. Vamos então dar uma olhada mais atenta aos padrões.

Fica aqui um exemplo do uso de padrões, para a declaração de variáveis, apenas para relembrar:

let (x, y) = (10, 15);

Existe um ponto a ter em consideração, tal como qualquer outro lugar onde uma nova variável é criada, o shadowing é introduzido. Por exemplo:

let x = 1;
let c = 'c';

match c {
x => println!("x: {}, c: {}", x, c)
}

println!("x: {}", x);

Isto imprime:

x: c, c: c
x: 1

Isto acontece porque, x => faz matching com o padrão e cria uma nova variável x. Esta nova variável "vive" dentro de um novo scope correspondente ao braço da expressão equivalente ao match. Assim sendo o x imprimido pelo primeiro println! corresponde à nova variável e o segundo a inicialmente declarada com o valor 1.

Múltiplos Padrões

Usando um pipe (|) podemos fazer múltiplos matchs no mesmo braço:

let x = 1;

match x {
1 | 2 => println!("um ou dois"),
3 => println!("três"),
_ => println!("Outro"),
}

Neste caso o código acima imprime, “um ou dois”.

Destructuring

No próximo artigo iremos falar sobre structs (estruturas), mas por agora o que precisa de saber é que se trata de um tipo de dados complexo, composto por um ou mais membros.

O destructuring permite decompor um tipo de dados composto em variáveis simples, como pode ver no exemplo abaixo:

struct Coordenada {
x: i32,
y: i32,
}

let ponto_de_interesse = Coordenada { x: 45, y: 89 };

match ponto_de_interesse {
Coordenada { x, y } => println!("Coordenada ({}, {})", x, y),
}

No exemplo acima, criamos uma nova estrutura Coordenada que é composta por dois inteiro (x e y), depois foi criada uma nova coordenada (ponto_de_interesse) e por fim decompusemos a Coordenada em x e y.

A decomposição de tipos também permite renomear os membros. Para isso faz-se uso de dois pontos (:):

struct Coordenada {
x: i32,
y: i32,
}

let ponto_de_interesse = Coordenada { x: 45, y: 89 };

match ponto_de_interesse {
Coordenada { x: novo_x, y: novo_y } => println!("Coordenada ({}, {})", novo_x, novo_y),
}

Mas, e quando não queremos saber de todos os valores? A resposta está no código seguinte:

match ponto_de_interesse {
Coordenada { x, .. } => println!("X tem o valor de {}", x),
}

Note que pode escolher os membros que deseja independentemente da sua ordem.

A decomposição de tipos também funciona com tuplos e enumerações.

Ignorar Variáveis

Usando um underscore (_) é possível ignorar tipos e valores, por exemplo, aqui neste match contra um Result<T, E>:

match valor {
OK(v) => println!("valor: {}", v),
Err(_) => println!("ocorreu um erro"),
}

No primeiro braço, nós apanhamos o valor da variável valor na variante Ok. Mas no braço do Err, usamos o _ para descartar o erro especificado e imprimimos uma mensagem geral.

Este tipo de mecanismo pode ser usado em outras áreas, como por exemplo ao fazer a desconstrução de um tuplo:

let (x, _, z) = cordenada_3d();

Aqui, apanhamos o primeiro e ultimo elemento do tuplo e ignoramos o do meio.

Uma nota importante, quando é feito o descartar de um elemento esse elemento não é movido, assim sendo é possível voltar a usar a variável original.

let tuple: (u32, String) = (5, String::from("cinco"));

// Aqui, o tuplo é movido, porque a String foi movida
let (x, _s) = tuple;

// a linha abaixo iria dar error, porque o valor do tuplo foi movido
// println!("O tuplo é: {:?}", tuple);

// Contudo,

let tuple = (5, String::from("cinco"));

// Aqui, o tuplo não é movido porque a String foi descartada e o u32 foi copiado
let (x, _) = tuple;

// Assim sendo, isto funciona:
println!("O tuplo é: {:?}", tuple);

ref e ref mut

Existe ainda uma palavra reservada usada para obter um referência dentro de um match:

let x = 10;

match x {
ref r => println!("É uma referência para {}", r),
}

Neste caso, r é do tipo &i32. Em outras palavras, o ref permite a criação de uma referência para ser usada no scope correspondente ao braço que fez match. Para obter uma referência mutável apenas necessita de acrescentar a palavra mut:

let mut x = 10;

match x {
ref mut r => println!("É uma referência mutável para {}", r),
}

Intervalos

Também é possível fazer match de intervalos (ranges) usando três pontos (...):

let valor = 10;

match valor {
1 ... 5 => println!("Entre um e cinco"),
_ => println!("Outro"),
}

Os intervalos são muito usados com inteiros e com caracteres:

let valor = '😌';

match valor {
'a' ... 'j' => println!("Primeira parte do alfateto"),
'k' ... 'z' => println!("Segunda parte do alfabeto"),
_ => println!("Outro caracter"),
}

Bindings

Quando estamos a trabalhar com tipos de dados compostos ou a fazer match com intervalos, podemos querer obter o valor de um membro em especifico ou então do valor que fez match, respetivamente:

let x = 3;

match x {
e @ 1 ... 5 => println!("o valor {} corresponde com o intervalo", e),
_ => println!("Outro"),
}

O exemplo anterior imprime “o valor 3 corresponde com o intervalo”. Agora com uma struct ficaria algo como:

#[derive(Debug)]
struct Carro {
marca: Option<String>
}

let marca = "Tesla".to_string();
let model_s: Option<Carro> = Some(Carro { marca: Some(marca) });

match model_s {
Some(Carro { marca: ref m @ Some(_), .. }) => println!("{:?}", m),
_ => { }
}

O exemplo acima imprime: Some("Tesla"). Em que a propriedade marca foi ligada a m.

Atenção, quando usa @ com | é necessário definir a ligação em cada parte:

let x = 5;

match x {
n @ 1 ... 5 | n @ 8 ... 10 => println!("O valor é {}", n),
_ => { },
}

Guards

Os guards permitem usar um padrão juntamente com um if. O if é aplicado em todas as partes do padrão:

let x = 4;
let y = false;

match x {
4 | 5 if y => println!("Sim"),
_ => println!("Não"),
}

Para usar um if por parte é necessário usar parênteses:

4 | (5 if y) => { }

Os padrões são muito poderosos e estão presentes em muitas partes da linguagem. Por isso faça bom uso deles.

Strings

Como referido no inicio deste artigo, vamos terminar a falar sobre Strings.

As Strings são uma parte importante em qualquer linguagem de programação. Em Rust as strings funcionam de uma forma um pouco diferente de outras linguagens, devido ao seu foco no sistema. Sempre que temos uma estrutura de dados de tamanho variável, as coisas podem ficar um pouco complicadas e as strings são um exemplo dessas estruturas. Isso quer dizer que as strings no Rust funcionam de uma forma diferente de outras linguagens de sistemas, tal como o C.

Mas vamos analisar isto em mais detalhe. Uma string é uma sequência de valores escalares Unicode codificados como uma sequência de bytes UTF-8. É garantido que todas as strings são uma sequência valida UTF-8. Adicionalmente, ao contrário de outras linguagens de sistema, as strings não terminam com um NUL-terminator e podem conter bytes NUL.

O Rust tem dois tipos de strings: &str e String. Vamos começar primeiro pela &str. Este tipo é normalmente referenciado como "string slices", basicamente são de tamanho fixo, não podem ser mutadas e são uma referência para uma sequência de bytes UTF-8.

// &'static str
let boas = "Olá, tudo bem?";

A string é um literal e é do tipo &'static str. Uma string literal é uma string que é alocada de forma estática, isso quer dizer que ela é guardada dentro do programa compilado e existe durante toda a execução do mesmo.

As strings literais podem ser divididas em múltiplas linhas. Existem duas formas de o fazer. A primeira forma inclui um nova linhas e o espaço à esquerda:

let s = "Isto é
um exemplo!";

assert_eq!("Isto é\n um exemplo!");

A segunda forma, permite escrever uma string de forma continua, sem espaçamento e novas linhas:

let s = "Isto é \
um exemplo!";

assert_eq!("Isto é um exemplo!");

Note que não é possível aceder a uma string do tipo str diretamente, mas sim apenas através de uma referência &str. Isto porque o str é um tipo de tamanho variável, então necessita de mais alguma informação de runtime para ser possível ser usado.

Mas o Rust tem mais do que o tipo &str. O tipo String trata-se de uma string alocada na heap. Este tipo de string pode alterar de tamanho e também é garantido que seja uma string UTF-8. Strings são normalmente criadas através de literais, usando o método to_string:

// é do tipo String
let mut s = "Olá".to_string();

// acrescenta ", tudo bem?" ao final da string
s.push_str(", tudo bem?");

As Strings são convertidas para &str usando o &:

fn takes_slice(slice: &str) {
println!("Tem: {}", slice);
}

fn main() {
let s = "Olá!".to_string();
takes_slice(&s);
}

Converter uma String pata &str é barato, mas converter uma &str para String envolve alocação de memória. Faça isso apenas quando for estritamente necessário.

Indicés

Uma vez que as strings são UTF-8 não é possível aceder diretamente a um índice. Por norma, aceder a um vetor é extremamente rápido. Mas, como os caracteres em UTF-8 têm tamanhos variáveis não é possível aceder diretamente a uma posição sem que se tenha que percorrer a string.

Existe duas formas de ver uma string, como bytes individuais ou como codepoints:

let better = "より良い場所";

for c in better.as_bytes() {
print!("{}, ", c);
}

println!("");

for c in better.chars() {
print!("{}, ", c);
}

println!("");

O resultado é:

227, 130, 136, 227, 130, 138, 232, 137, 175, 227, 129, 132, 229, 160, 180, 230, 137, 128,
よ, り, 良, い, 場, 所,

Como pode ver existem mais bytes do que caracteres. Para obter algo muito semelhante a um indice, pode usar:

// algo tipo `better[1]`
let caracter = better.chars().nth(1);

Divisão

Para obter uma parte da string usamos a seguinte sintaxe:

let cao = "Alec Sadler";
let primeiro_nome = &cao[0..4];

Lembre-se que isto são offsets de bytes e não de caracteres. Por isso isto vai falhar durante o tempo de execução:

let love_world = "❤️🌍😌";
let dois_primeiros = &love_world[0..2];

Com o erro:

thread 'main' panicked at 'index 0 and/or 2 in `❤️🌍😌` do not lie on character boundary', ../src/libcore/str/mod.rs:1721

Concatenação

Para concatenar uma &str no final de uma String usamos o operador +:

let ola = "Olá!".to_string();
let tudo = " Tudo bem?";

let ola_tudo = ola + tudo;

Quando tem duas Strings, é necessário usar um &:

let ola = "Olá!".to_string();
let tudo = " Tudo bem?".to_String();

let ola_tudo = ola + &tudo;

Isto porque &String consegue-se automaticamente converter em um &str. Isto tem o nome de Deref coercions, mas isso fica para um artigo futuro.

Por hoje vamos ficar por aqui e no próximo artigo vamos falar sobre Structs, ou tipos de dados compostos. Aí sim, a coisa irá começar a ficar ainda mais interessante.

Por agora é tudo, fiquem em paz e boas codificações! 🙂

--

--