A construção de fluxo de controle match
Rust tem uma construção de fluxo de controle extremamente poderosa chamada
match, que permite comparar um valor com uma série de padrões e então
executar código com base em qual padrão corresponde. Padrões podem ser formados
por valores literais, nomes de variáveis, curingas e muitas outras coisas; o
Capítulo 19 aborda todos os tipos de padrões
e o que eles fazem. O poder de match vem da expressividade desses padrões e
do fato de o compilador confirmar que todos os casos possíveis foram tratados.
Pense em uma expressão match como uma máquina de classificar moedas: as
moedas deslizam por uma trilha com buracos de tamanhos variados, e cada moeda
cai no primeiro buraco em que ela cabe. Da mesma forma, valores passam por cada
padrão de um match, e, no primeiro padrão em que o valor “se encaixa”, ele
cai no bloco de código associado para ser usado durante a execução.
Já que falamos em moedas, vamos usá-las como exemplo com match! Podemos
escrever uma função que recebe uma moeda americana desconhecida e, de forma
parecida com uma máquina de contagem, determina qual moeda é e retorna seu
valor em centavos, como mostra a Listagem 6-3.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {}
match que usa as variantes desse enum como padrõesVamos destrinchar o match da função value_in_cents. Primeiro, temos a
palavra-chave match, seguida por uma expressão, que neste caso é o valor
coin. Isso parece bastante com uma expressão condicional usada com if, mas
há uma grande diferença: com if, a condição precisa ser avaliada para um
valor booleano; aqui, ela pode ser de qualquer tipo. O tipo de coin, neste
exemplo, é o enum Coin que definimos na primeira linha.
Depois vêm os braços do match. Um braço tem duas partes: um padrão e algum
código. O primeiro braço aqui tem como padrão o valor Coin::Penny e, em
seguida, o operador =>, que separa o padrão do código a ser executado. O
código, neste caso, é apenas o valor 1. Cada braço é separado do seguinte
por uma vírgula.
Quando a expressão match é executada, ela compara o valor resultante com o
padrão de cada braço, em ordem. Se um padrão corresponder ao valor, o código
associado a esse padrão é executado. Se esse padrão não corresponder, a
execução continua para o próximo braço, como numa máquina de classificar
moedas. Podemos ter quantos braços precisarmos: na Listagem 6-3, nosso match
tem quatro braços.
O código associado a cada braço é uma expressão, e o valor resultante da
expressão no braço correspondente é o valor retornado pela expressão match
inteira.
Normalmente não usamos chaves quando o código do braço é curto, como na
Listagem 6-3, em que cada braço apenas retorna um valor. Se você quiser
executar várias linhas de código em um braço de match, deve usar chaves, e a
vírgula após esse braço passa então a ser opcional. Por exemplo, o código a
seguir imprime “Lucky penny!” toda vez que o método é chamado com
Coin::Penny, mas ainda retorna o último valor do bloco, 1:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {}
Padrões que se ligam a valores
Outro recurso útil dos braços de match é que eles podem se ligar às partes
dos valores que correspondem ao padrão. É assim que podemos extrair valores de
variantes de enum.
Como exemplo, vamos alterar uma das variantes do nosso enum para armazenar
dados dentro dela. De 1999 a 2008, os Estados Unidos cunharam moedas de 25
centavos com desenhos diferentes para cada um dos 50 estados em um dos lados.
Nenhuma outra moeda recebeu desenhos de estados, então apenas os quarters têm
esse valor extra. Podemos adicionar essa informação ao nosso enum alterando a
variante Quarter para incluir um valor UsState armazenado dentro dela, como
fizemos na Listagem 6-4.
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {}
Coin em que a variante Quarter também armazena um valor UsStateVamos imaginar que um amigo esteja tentando colecionar todos os 50 quarters de estados. Enquanto classificamos nosso troco por tipo de moeda, também diremos o nome do estado associado a cada quarter para que, se for um que nosso amigo não tenha, ele possa adicioná-lo à coleção.
Na expressão match deste código, adicionamos uma variável chamada state ao
padrão que corresponde a valores da variante Coin::Quarter. Quando um
Coin::Quarter corresponder, a variável state será ligada ao valor do estado
daquele quarter. Depois, podemos usar state no código desse braço, assim:
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {state:?}!");
25
}
}
}
fn main() {
value_in_cents(Coin::Quarter(UsState::Alaska));
}
Se chamássemos value_in_cents(Coin::Quarter(UsState::Alaska)), coin seria
Coin::Quarter(UsState::Alaska). Quando comparamos esse valor com cada um dos
braços do match, nenhum deles corresponde até chegarmos a
Coin::Quarter(state). Nesse ponto, a ligação state terá o valor
UsState::Alaska. Podemos então usar essa ligação na expressão println!,
obtendo assim o valor interno do estado da variante Coin::Quarter.
O padrão match com Option<T>
Na seção anterior, queríamos obter o valor interno T do caso Some ao usar
Option<T>; também podemos tratar Option<T> com match, assim como fizemos
com o enum Coin! Em vez de comparar moedas, vamos comparar as variantes de
Option<T>, mas a forma como a expressão match funciona continua a mesma.
Digamos que queremos escrever uma função que recebe um Option<i32> e, se
houver um valor dentro, soma 1 a esse valor. Se não houver valor algum, a
função deve retornar None e não tentar executar operação nenhuma.
Essa função é muito fácil de escrever graças a match, e ficará como a
Listagem 6-5.
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
match sobre um Option<i32>Vamos examinar a primeira execução de plus_one com mais detalhes. Quando
chamamos plus_one(five), a variável x no corpo de plus_one terá o valor
Some(5). Em seguida, comparamos isso com cada braço do match:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
O valor Some(5) não corresponde ao padrão None, então seguimos para o
próximo braço:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Some(5) corresponde a Some(i)? Sim! Temos a mesma variante. O i se liga
ao valor contido em Some, então i recebe o valor 5. O código daquele
braço do match é então executado, somamos 1 ao valor de i e criamos um
novo valor Some com o total 6 dentro.
Agora vamos considerar a segunda chamada de plus_one na Listagem 6-5, em que
x é None. Entramos no match e comparamos com o primeiro braço:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Ele corresponde! Não há valor a ser somado, então o programa para e retorna o
valor None que está do lado direito de =>. Como o primeiro braço
correspondeu, nenhum outro braço é comparado.
Combinar match e enums é útil em muitas situações. Você verá bastante esse
padrão em código Rust: fazer match sobre um enum, ligar uma variável aos
dados internos e então executar código com base nisso. No começo ele parece um
pouco complicado, mas, depois que você se acostuma, acaba desejando ter isso em
todas as linguagens. É consistentemente um dos recursos favoritos dos usuários.
Matches são exaustivos
Há outro aspecto de match que precisamos discutir: os padrões dos braços
devem cobrir todas as possibilidades. Considere esta versão da nossa função
plus_one, que tem um bug e não compila:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Não tratamos o caso None, então esse código causará um bug. Felizmente, esse
é um bug que Rust sabe capturar. Se tentarmos compilar esse código, obteremos
este erro:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
--> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597:5
|
= note: not covered
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error
Rust sabe que não cobrimos todos os casos possíveis e até sabe qual padrão foi
esquecido! Matches em Rust são exaustivos: precisamos cobrir até a última
possibilidade para que o código seja válido. Especialmente no caso de
Option<T>, quando Rust nos impede de esquecer de tratar explicitamente o caso
None, ela nos protege de assumir que temos um valor quando na verdade
poderíamos ter null, tornando impossível o erro de um bilhão de dólares
discutido antes.
Padrões pega-tudo e o marcador _
Usando enums, também podemos realizar ações especiais para alguns valores
específicos e, para todos os outros, executar uma ação padrão. Imagine que
estamos implementando um jogo em que, se você tirar 3 em uma rolagem de dado,
seu personagem não se move, mas ganha um chapéu novo e elegante. Se você tirar
7, seu personagem perde um chapéu elegante. Para todos os outros valores, seu
personagem anda aquele número de casas no tabuleiro. Aqui está um match que
implementa essa lógica, com o resultado do dado fixado no código em vez de ser
um valor aleatório, e toda a outra lógica representada por funções sem corpo,
porque implementá-las de verdade foge do escopo deste exemplo:
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
}
Nos dois primeiros braços, os padrões são os valores literais 3 e 7. Para
o último braço, que cobre todos os outros valores possíveis, o padrão é a
variável que escolhemos chamar de other. O código executado para o braço
other usa essa variável ao passá-la para a função move_player.
Esse código compila mesmo sem listarmos todos os valores possíveis de um u8,
porque o último padrão corresponderá a todos os valores não listados
explicitamente. Esse padrão pega-tudo atende ao requisito de que match deve
ser exaustivo. Observe que precisamos colocar o braço pega-tudo por último,
porque os padrões são avaliados em ordem. Se tivéssemos colocado o braço
pega-tudo antes, os outros braços nunca seriam executados, então Rust nos
avisará se adicionarmos braços depois de um pega-tudo.
Rust também tem um padrão que podemos usar quando queremos um pega-tudo, mas
não queremos usar o valor capturado por ele: _ é um padrão especial que
corresponde a qualquer valor e não se liga a ele. Isso diz ao Rust que não
vamos usar o valor, então ela não nos avisará sobre uma variável não utilizada.
Vamos mudar as regras do jogo: agora, se você tirar qualquer valor diferente de
3 ou 7, deve rolar novamente. Não precisamos mais usar o valor pega-tudo,
então podemos alterar o código para usar _ em vez da variável chamada
other:
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
}
Este exemplo também atende ao requisito de exaustividade porque estamos explicitamente ignorando todos os outros valores no último braço; não esquecemos nada.
Por fim, vamos mudar as regras do jogo mais uma vez, de modo que nada mais
aconteça no turno se você tirar qualquer coisa diferente de 3 ou 7. Podemos
expressar isso usando o valor unitário, o tipo de tupla vazia mencionado na
seção “O tipo tupla”, como o código associado ao braço
_:
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
}
Aqui, estamos dizendo explicitamente ao Rust que não vamos usar nenhum outro valor que não corresponda a um padrão de um braço anterior e que também não queremos executar código algum nesse caso.
Há mais sobre padrões e correspondência no Capítulo
19. Por enquanto, vamos seguir para a sintaxe
if let, que pode ser útil em situações em que a expressão match fica um
pouco verbosa.