Operador match de Controle de Fluxo

O Rust tem um excelente operador de controle de fluxo chamado match, que nos permite comparar um valor com uma série de padrões e executar um código com base no padrão que casar. Padrões podem ser compostos de valores literais, variáveis, caracteres-curinga e várias outras coisas. O Capítulo 18 aborda todos os tipos de padrões e o que eles fazem. A grande utilidade do match vem da expressividade dos padrões e das análises feitas pelo compilador, tendo certeza de que todos os casos possíveis estão sendo tratados.

Imagine que expressão match funciona como uma máquina de contar moedas: as moedas passam por um canal que possui furos de vários tamanhos, e cada moeda cai no primeiro furo em que ela couber. Da mesma forma, os valores passam por cada padrão de um match, e logo no primeiro padrão que o valor "se encaixar", o bloco de código que estiver associado a ele será executado.

Aproveitando que acabamos de falar sobre moedas, vamos usá-las como exemplo de utilização do match! Podemos escrever uma função que recebe uma moeda qualquer dos Estados Unidos e, assim como uma máquina, determina qual moeda ela é e retorna seu valor em cents, como mostra a Listagem 6-3:

Nota do tradutor: diferentemente do que acontece na maioria dos países, as moedas dos Estados Unidos possuem nomes: as de 1 cent são chamadas de Penny; as de 5 cents, de Nickel; as de 10 cents, de Dime; e as de 25 cents, de Quarter.


# #![allow(unused_variables)]
#fn main() {
enum Moeda {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn valor_em_cents(moeda: Moeda) -> u32 {
    match moeda {
        Moeda::Penny => 1,
        Moeda::Nickel => 5,
        Moeda::Dime => 10,
        Moeda::Quarter => 25,
    }
}
#}

Listagem 6-3: Uma enum e uma expressão match em que os padrões comparados são as variantes da enum.

Vamos analisar o match da função valor_em_cents. Primeiro, usamos a palavra-chave match seguida de uma expressão, que neste caso é o valor moeda. É parecido a uma expressão utilizada com if, mas tem uma grande diferença: com if, a expressão precisa retornar um valor booleano. Aqui, pode ser de qualquer tipo. O tipo da variável moeda, neste exemplo, é a enum Moeda, que definimos na Listagem 6-3.

Em seguida vêm os braços do match. Um braço é composto por duas partes: um padrão e algum código. O primeiro braço deste exemplo tem um padrão, que é o valor Moeda::Penny, e o operador => que separa o padrão do código associado a ele. O código, nesse caso, é apenas o valor 1. Os braços são separados uns dos outros com uma vírgula.

Quando o match é executado, ele compara o valor resultante com o padrão de cada braço, na ordem. Se o valor casar com um determinado padrão, o código associado a esse padrão será executado. Se o valor não se encaixa nesse padrão, a execução passa para o próximo braço, bem parecido com a máquina de contar moedas. Podemos ter tantos braços quanto precisamos. No caso da Listagem 6-3, nosso match tem quatro braços.

O código associado a cada braço é uma expressão, e o seu valor resultante, no braço que combinar, é o que será retornado pela expressão match.

Tipicamente não se usa chaves se o braço do match for curto, como é o caso na Listagem 6-3, em que cada braço retorna apenas um valor. Se você quiser executar mais de uma linha de código em um braço, você pode usar chaves para delimitá-las. Por exemplo, o código seguinte vai escrever na tela "Moeda da sorte!" sempre que o método for chamado com uma Moeda::Penny, mas ainda vai retornar o último valor do bloco, 1:


# #![allow(unused_variables)]
#fn main() {
# enum Moeda {
#    Penny,
#    Nickel,
#    Dime,
#    Quarter,
# }
#
fn valor_em_cents(moeda: Moeda) -> u32 {
    match moeda {
        Moeda::Penny => {
            println!("Moeda da sorte!");
            1
        },
        Moeda::Nickel => 5,
        Moeda::Dime => 10,
        Moeda::Quarter => 25,
    }
}
#}

Padrões Atrelados a Valores

Outra característica útil dos braços do match é que eles podem ser atrelados a partes dos valores que se encaixam no padrão. É assim que podemos extrair valores dentro de uma variante de uma enum.

Por exemplo, vamos alterar uma das nossas variantes, inserindo dados dentro dela. De 1999 até 2008, os Estados Unidos cunhou quarters com um design diferente para cada um dos 50 estados em um dos lados da moeda. Nenhuma outra moeda tinha essa diferença no design, apenas os quarters. Podemos adicionar essa informação à nossa enum alterando a variante Quarter para incluir o valor Estado, como é feito na Listagem 6-4:


# #![allow(unused_variables)]
#fn main() {
#[derive(Debug)] // Para podermos ver qual é o estado com mais facilidade
enum Estado {
    Alabama,
    Alaska,
    // ... etc
}

enum Moeda {
    Penny,
    Nickel,
    Dime,
    Quarter(Estado),
}
#}

Listagem 6-4: Enum Moeda, cuja variante Quarter também guarda o valor Estado.

Vamos imaginar que um amigo nosso está tentando colecionar todas os quarters dos 50 estados. Enquanto separamos nosso troco por tipo de moeda, vamos também dizer o nome do estado associado a cada quarter. Se for um dos que o nosso amigo ainda não tem, ele pode colocá-lo na sua coleção.

Na expressão match desse código, vamos adicionar uma variável chamada estado ao padrão que casa com os valores da variante Moeda::Quarter. Quando uma Moeda::Quarter é testada, a variável estado vai ser atrelada ao valor do estado daquele quarter. Assim vamos poder usar o estado no código do braço, desse jeito:


# #![allow(unused_variables)]
#fn main() {
# #[derive(Debug)]
# enum Estado {
#    Alabama,
#    Alaska,
# }
#
# enum Moeda {
#    Penny,
#    Nickel,
#    Dime,
#    Quarter(Estado),
# }
#
fn valor_em_cents(moeda: Moeda) -> u32 {
    match moeda {
        Moeda::Penny => 1,
        Moeda::Nickel => 5,
        Moeda::Dime => 10,
        Moeda::Quarter(estado) => {
            println!("Quarter do estado {:?}!", estado);
            25
        },
    }
}
#}

Se executarmos valor_em_cents(Moeda::Quarter(Estado::Alaska)), moeda seria Moeda::Quarter(Estado::Alaska). Quando comparamos esse valor em cada um dos braços do match, nenhum deles vai casar enquanto não chegar em Moeda::Quarter(estado). Nesse ponto, estado vai estar atrelado ao valor Estado::Alaska. Podemos, então, usar esse valor na expressão println!, obtendo o estado contido na variante Quarter da enum Moeda.

Usando match com Option<T>

Na seção anterior, queríamos obter o valor T contido em um Some quando era o caso em uma Option<T>. Também podemos manipular uma Option<T> usandomatch, assim como fizemos com a enumMoeda! Em vez de comparar moedas, vamos comparar as variantes deOption, mas a forma de trabalhar com a expressãomatch` continua a mesma.

Digamos que queremos escrever uma função que recebe um Option<i32>, e se houver um valor embutido nele, soma um a esse valor. Se não houver um valor, a função deve retornar None, e nem tentar executar nenhuma operação.

Essa função é bem fácil de implementar, graças ao match, e vai ficar conforme visto na Listagem 6-5:


# #![allow(unused_variables)]
#fn main() {
fn mais_um(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

let cinco = Some(5);
let seis = mais_um(cinco);
let nenhum = mais_um(None);
#}

Listagem 6-5: Uma função que usa um match para tratar uma Option<i32>.

Casando Some(T)

Vamos examinar a primeira execução de mais_um em mais detalhes. Quando chamamos mais_um(cinco), a variável x no corpo da função mais_um vai ter o valor Some(5). Então comparamos ele a cada braço do match.

None => None,

O valor Some(5) não casa com o padrão None, então seguimos para o próximo braço.

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

Some(5) casa comSome(i)? Sim, casa! Temos a mesma variante. Oiestá atrelado ao valor contido emSome, entãoipassa a ter o valor5. O código desse braço é executado, então somamos um ao valor deie criamos um novoSomecontendo nosso total de6`.

Casando None

Agora vamos considerar a segunda chamada da função mais_um na Listagem 6-5, em que x é None. Nós entramos no match e comparamos com o primeiro braço.

None => None,

Confere! Não há nenhum valor para somar, então o programa pára e retorna o valor None do lado direito do =>. Como o primeiro braço já casou, nenhum dos demais será testado.

A combinação de enums e a expressão match é útil em diversas situações. Você verá muito esse padrão em Rust: fazer o match de uma enum, associar uma variável ao valor embutido, e então executar um código baseado nele. Pode parecer complicado no começo, mas uma vez que você se acostume, você vai querer que tivesse isso em todas as linguagens. É, sistematicamente, um favorito dos usuários.

Matches São Exaustivos

Há outro aspecto do match que precisamos discutir. Considere essa versão da nossa função mais_um:

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

Nós não tratamos o caso None, logo vai ocorrer um bug no nosso código. Por sorte, é um bug que o Rust sabe detectar. Se tentarmos compilar esse código, vamos ter esse erro:

error[E0004]: non-exhaustive patterns: `None` not covered
 -->
  |
6 |         match x {
  |               ^ pattern `None` not covered

O Rust sabe que nós não cobrimos todos os casos possíveis, e sabe até de qual padrão nos esquecemos! Matches em Rust são exaustivos: precisamos extinguir até a última possibilidade pra que o nosso código seja válido. Especialmente no caso de uma Option<T>, em que o Rust não nos deixa esquecer de tratar explicitamente o caso None. Ele nos impede de assumir que temos um valor válido quando possivelmente temos um valor nulo, e portanto, cometer o erro de um bilhão de dólares que vimos mais cedo.

The _ Placeholder

O Placeholder _

O Rust também tem um padrão que podemos usar em situações em que não queremos listar todos os valores possíveis. Por exemplo, um u8 pode ter valores válidos de 0 a 255. Se nos importamos apenas com os valores 1, 3, 5 e 7, não queremos ser obrigados a listar o 0, 2, 4, 6, 8, 9, e assim por diante até 255. Felizmente, nem precisamos: em vez disso, podemos usar o padrão especial _.


# #![allow(unused_variables)]
#fn main() {
let algum_valor_u8 = 0u8;
match algum_valor_u8 {
    1 => println!("um"),
    3 => println!("três"),
    5 => println!("cinco"),
    7 => println!("sete"),
    _ => (),
}
#}

O padrão _ casa com qualquer valor. Colocando ele depois dos demais braços, o _ vai casar com todos os casos possíveis que não foram especificados antes dele. O () é só o valor-unidade, pra que nada aconteça no caso _. Como resultado, podemos dizer que não queremos fazer nada com os possíveis valores que não listamos antes do placeholder _.

Contudo, a expressão match pode ser um tanto verbosa em uma situação em que queremos apenas lidar com um dos casos. Pra essa situação, o Rust oferece o if let.