A Construção de Fluxo de Controle Match

Beginner

This tutorial is from open-source community. Access the source code

Introdução

Bem-vindo à Construção de Fluxo de Controle match. Este laboratório faz parte do Livro Rust. Você pode praticar suas habilidades em Rust no LabEx.

Neste laboratório, exploraremos a poderosa construção de fluxo de controle match em Rust, que permite a correspondência de padrões e a execução de código com base no padrão correspondido.

A Construção de Fluxo de Controle match

Rust possui uma construção de fluxo de controle extremamente poderosa chamada match que permite comparar um valor com uma série de padrões e, em seguida, executar código com base em qual padrão corresponde. Os padrões podem ser compostos por valores literais, nomes de variáveis, curingas e muitas outras coisas; o Capítulo 18 cobre todos os diferentes tipos de padrões e o que eles fazem. O poder do match vem da expressividade dos padrões e do fato de que o compilador confirma que todos os casos possíveis são tratados.

Pense em uma expressão match como uma máquina de classificação de moedas: as moedas deslizam por uma trilha com orifícios de vários tamanhos ao longo dela, e cada moeda cai pelo primeiro orifício que encontra e que cabe nela. Da mesma forma, os valores passam por cada padrão em um match, e no primeiro padrão em que o valor "se encaixa", o valor cai no bloco de código associado para ser usado durante a execução.

Falando em moedas, vamos usá-las como exemplo usando match! Podemos escrever uma função que recebe uma moeda dos EUA desconhecida e, de maneira semelhante à máquina de contagem, determina qual moeda é e retorna seu valor em centavos, conforme mostrado na Listagem 6-3.

1 enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
  2 match coin {
      3 Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Listagem 6-3: Um enum e uma expressão match que tem as variantes do enum como seus padrões

Vamos detalhar o match na função value_in_cents. Primeiro, listamos a palavra-chave match seguida por uma expressão, que neste caso é o valor coin [2]. Isso parece muito semelhante a uma expressão usada com if, mas há uma grande diferença: com if, a expressão precisa retornar um valor booleano, mas aqui ela pode retornar qualquer tipo. O tipo de coin neste exemplo é o enum Coin que definimos em [1].

Em seguida, vêm os braços match. Um braço tem duas partes: um padrão e algum código. O primeiro braço aqui tem um padrão que é o valor Coin::Penny e, em seguida, o operador => que separa o padrão e o código a ser executado [3]. O código neste caso é apenas o valor 1. Cada braço é separado do próximo 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 ao valor, a execução continua para o próximo braço, assim como em uma máquina de classificação de 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 que é retornado para toda a expressão match.

Normalmente, não usamos chaves se o código do braço match for curto, como é na Listagem 6-3, onde cada braço apenas retorna um valor. Se você quiser executar várias linhas de código em um braço match, você deve usar chaves, e a vírgula após o braço é então opcional. Por exemplo, o código a seguir imprime "Lucky penny!" toda vez que o método é chamado com um Coin::Penny, mas ainda retorna o último valor do bloco, 1:

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Padrões que se Vinculam a Valores

Outra característica útil dos braços match é que eles podem se vincular às partes dos valores que correspondem ao padrão. É assim que podemos extrair valores das variantes enum.

Como exemplo, vamos alterar uma de nossas variantes enum para conter dados dentro dela. De 1999 a 2008, os Estados Unidos cunharam moedas de 25 centavos com designs diferentes para cada um dos 50 estados em um lado. Nenhuma outra moeda recebeu designs estaduais, então apenas as moedas de 25 centavos 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, o que 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),
}

Listagem 6-4: Um enum Coin no qual a variante Quarter também contém um valor UsState

Vamos imaginar que um amigo está tentando colecionar todas as moedas de 25 centavos dos 50 estados. Enquanto classificamos nossa troco por tipo de moeda, também diremos o nome do estado associado a cada moeda de 25 centavos para que, se for uma que nosso amigo não tem, ele possa adicioná-la à sua coleção.

Na expressão match para este código, adicionamos uma variável chamada state ao padrão que corresponde aos valores da variante Coin::Quarter. Quando um Coin::Quarter corresponde, a variável state se vinculará ao valor do estado dessa moeda de 25 centavos. Então, podemos usar state no código para esse braço, assim:

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
        }
    }
}

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 match, nenhum deles corresponde até chegarmos a Coin::Quarter(state). Nesse ponto, a ligação para state será o valor UsState::Alaska. Podemos então usar essa ligação na expressão println!, obtendo assim o valor do estado interno da variante enum Coin para Quarter.

Correspondência com Option<T>

Na seção anterior, queríamos obter o valor interno T do caso Some ao usar Option<T>; também podemos lidar com Option<T> usando match, como fizemos com o enum Coin! Em vez de comparar moedas, compararemos as variantes de Option<T>, mas a maneira como a expressão match funciona permanece a mesma.

Digamos que queremos escrever uma função que recebe um Option<i32> e, se houver um valor dentro, adiciona 1 a esse valor. Se não houver um valor dentro, a função deve retornar o valor None e não tentar realizar nenhuma operação.

Esta função é muito fácil de escrever, graças ao match, e se parecerá com a Listagem 6-5.

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
      1 None => None,
      2 Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five); 3
let none = plus_one(None); 4

Listagem 6-5: Uma função que usa uma expressão match em um Option<i32>

Vamos examinar a primeira execução de plus_one em mais detalhes. Quando chamamos plus_one(five) [3], a variável x no corpo de plus_one terá o valor Some(5). Em seguida, comparamos isso com cada braço match:

None => None,

O valor Some(5) não corresponde ao padrão None [1], então continuamos para o próximo braço:

Some(i) => Some(i + 1),

Some(5) corresponde a Some(i) [2]? Sim, corresponde! Temos a mesma variante. O i se vincula ao valor contido em Some, então i assume o valor 5. O código no braço match é então executado, então adicionamos 1 ao valor de i e criamos um novo valor Some com nosso total 6 dentro.

Agora, vamos considerar a segunda chamada de plus_one na Listagem 6-5, onde x é None [4]. Entramos no match e comparamos com o primeiro braço [1].

Corresponde! Não há valor para adicionar, então o programa para e retorna o valor None no 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á esse padrão muito no código Rust: match contra um enum, vincular uma variável aos dados internos e, em seguida, executar o código com base nele. É um pouco complicado no início, mas assim que você se acostumar, desejará tê-lo em todas as linguagens. É consistentemente o favorito dos usuários.

Matches São Exaustivos

Há um outro aspecto do match que precisamos discutir: os padrões dos braços devem cobrir todas as possibilidades. Considere esta versão de nossa função plus_one, que tem um bug e não compilará:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

Não tratamos o caso None, então este código causará um bug. Felizmente, é um bug que o Rust sabe como detectar. Se tentarmos compilar este código, obteremos este erro:

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
      = 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!(),
    |

O Rust sabe que não cobrimos todos os casos possíveis e até sabe qual padrão esquecemos! Os matches em Rust são exaustivos: devemos esgotar todas as últimas possibilidades para que o código seja válido. Especialmente no caso de Option<T>, quando o Rust nos impede de esquecer de lidar explicitamente com o caso None, ele nos protege de presumir que temos um valor quando podemos ter nulo, tornando assim o erro de um bilhão de dólares discutido anteriormente impossível.

Padrões Catch-all e o Placeholder _

Usando enums, também podemos tomar ações especiais para alguns valores particulares, mas para todos os outros valores tomar uma ação padrão. Imagine que estamos implementando um jogo onde, se você rolar um 3 em uma jogada de dados, seu jogador não se move, mas em vez disso ganha um novo chapéu chique. Se você rolar um 7, seu jogador perde um chapéu chique. Para todos os outros valores, seu jogador se move aquele número de espaços no tabuleiro do jogo. Aqui está um match que implementa essa lógica, com o resultado da jogada de dados hardcoded em vez de um valor aleatório, e toda a outra lógica representada por funções sem corpos porque implementá-las realmente está fora do escopo deste exemplo:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
  1 other => move_player(other),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}

Para os 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 nomear other [1]. O código que é executado para o braço other usa a variável passando-a para a função move_player.

Este código compila, mesmo que não tenhamos listado todos os valores possíveis que um u8 pode ter, porque o último padrão corresponderá a todos os valores não listados especificamente. Este padrão catch-all atende ao requisito de que match deve ser exaustivo. Observe que temos que colocar o braço catch-all por último porque os padrões são avaliados em ordem. Se colocarmos o braço catch-all antes, os outros braços nunca serão executados, então o Rust nos avisará se adicionarmos braços após um catch-all!

O Rust também tem um padrão que podemos usar quando queremos um catch-all, mas não queremos usar o valor no padrão catch-all: _ é um padrão especial que corresponde a qualquer valor e não se vincula a esse valor. Isso diz ao Rust que não vamos usar o valor, então o Rust não nos avisará sobre uma variável não utilizada.

Vamos mudar as regras do jogo: agora, se você rolar qualquer coisa diferente de 3 ou 7, você deve rolar novamente. Não precisamos mais usar o valor catch-all, então podemos mudar nosso código para usar _ em vez da variável chamada other:

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.

Finalmente, mudaremos as regras do jogo mais uma vez para que nada mais aconteça na sua vez se você rolar qualquer coisa diferente de 3 ou 7. Podemos expressar isso usando o valor unitário (o tipo de tupla vazio que mencionamos em "O Tipo de Tupla") como o código que acompanha o braço _:

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 ao Rust explicitamente que não vamos usar nenhum outro valor que não corresponda a um padrão em um braço anterior, e não queremos executar nenhum código neste caso.

Há mais sobre padrões e correspondência que abordaremos no Capítulo 18. Por enquanto, vamos passar para a sintaxe if let, que pode ser útil em situações em que a expressão match é um pouco prolixa.

Resumo

Parabéns! Você concluiu o laboratório The Match Control Flow Construct. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.