segunda-feira, 5 de abril de 2021

O operador de controle de fluxo match

O Rust tem um operador de fluxo de controle extremamente poderoso, chamado, match que permite comparar um valor com uma série de padrões e, em seguida, executar o código com base nos padrões correspondentes. Os padrões podem ser compostos de 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 de match vem da expressividade dos padrões e do fato de que o compilador confirma que todos os casos possíveis foram tratados.

Pense em uma expressão match como uma máquina de separar 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 em que se encaixa. Da mesma forma, os valores passam por cada padrão em match, e no primeiro padrão o valor “se ajusta”, o valor cai no bloco de código associado a ser usado durante a execução.

Como acabamos de mencionar as moedas, vamos usá-las como exemplo usando match! Podemos escrever uma função que pode pegar uma moeda desconhecida dos Estados Unidos e, de maneira semelhante à máquina de contagem, determinar qual moeda ela é e retornar seu valor em centavos, conforme mostrado aqui na Listagem 6-3.

enum Moeda {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

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

fn main() {}

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

Vamos decompôr match na função valor_em_centavos. Primeiro, listamos a palavra-chave match seguida por uma expressão, que neste caso é o valor moeda. 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 pode ser de qualquer tipo. O tipo de moeda neste exemplo é o enum Moeda que definimos na linha 1.

Em seguida estão os braços de 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 Moeda::Penny e, em seguida, o operador => que separa o padrão e o código a ser executado. 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 será executado. Se esse padrão não corresponder ao valor, a execução continua para o próximo braço, da mesma forma que em uma máquina de classificação de moedas. Podemos ter quantos braços precisarmos: na Listagem 6-3, nossa expressão 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 a expressão match inteira.

Os colchetes normalmente não são usados ​​se o código do braço de correspondência for curto, como na Listagem 6-3, onde cada braço apenas retorna um valor. Se você deseja executar várias linhas de código em um braço de correspondência, pode usar chaves. Por exemplo, o código a seguir imprimiria “Lucky penny!” toda vez que o método foi chamado com um, Moeda::Penny mas ainda assim retornaria o último valor do bloco 1:

enum Moeda {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn valor_em_centavos(moeda: Moeda) -> u8 {
    match moeda {
        Moeda::Penny => {
            println!("Moeda da sorte!");
            1
        }
        Moeda::Nickel => 5,
        Moeda::Dime => 10,
        Moeda::Quarter => 25,
    }
}

fn main() {}

Padrões que se ligam a valores

Outro recurso útil dos braços de combinação é que eles podem se vincular às partes dos valores que correspondem ao padrão. É assim que podemos extrair valores de variantes enum.

Como exemplo, vamos alterar uma de nossas variantes de enum para conter os dados dentro dela. De 1999 a 2008, os Estados Unidos cunharam quartos com designs diferentes para cada um dos 50 estados de um lado. Nenhuma outra moeda tem design de estado, então apenas quartos têm esse valor extra. Podemos adicionar essas informações ao nosso enum alterando a variante Quarter para incluir um valor UsState armazenado dentro dela, o que fizemos aqui na Listagem 6-4.

#[derive(Debug)] // para que possamos inspecionar o estado em um minuto
enum UsState {
    Alabama,
    Alaska,
    // --recorte--
}

enum Moeda {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {}

Listagem 6-4: um enum Moeda em que a variante Quarter também possui um valor UsState.

Vamos imaginar que um amigo nosso está tentando coletar todos os 50 bairros estaduais. Enquanto classificamos nosso troco por tipo de moeda, também chamaremos o nome do estado associado a cada trimestre para que, se for um que nosso amigo não tenha, eles possam adicioná-lo à sua coleção.

Na expressão de correspondência para este código, adicionamos uma variável chamada state ao padrão que corresponde aos valores da variante Moeda::Quarter. Quando um Moeda::Quarter corresponde, a variável state se vincula ao valor do estado daquele trimestre. Então, podemos usar state no código para esse braço, assim:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --recorte--
}

enum Moeda {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn valor_em_centavos(moeda: Moeda) -> u8 {
    match moeda {
        Moeda::Penny => 1,
        Moeda::Nickel => 5,
        Moeda::Dime => 10,
        Moeda::Quarter(state) => {
            println!("quarter do Estado {:?}!", state);
            25
        }
    }
}

fn main() {
    valor_em_centavos(Moeda::Quarter(UsState::Alaska));
}

Se tivéssemos que chamar valor_em_centavos(Moeda::Quarter(UsState::Alaska)), moeda seria Moeda::Quarter(UsState::Alaska). Quando comparamos esse valor com cada um dos braços do jogo, nenhum deles combina até alcançarmos Moeda::Quarter(state). Nesse ponto, a vinculaçã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 Moeda para Quarter.

Combinando com Option<T>

Na seção anterior, queríamos obter o valor T interno de Some ao usar Option<T>; também podemos lidar com Option<T> usando match como fizemos com o enum Moeda! Em vez de comparar moedas, compararemos as variantes de Option<T>, mas a forma 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, adicione 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 a match, e será semelhante à Listagem 6-5.

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 uma expressão match num Option<i32>.

Vamos examinar a primeira execução de mais_um com mais detalhes. Quando chamamos mais_um(cinco), a variável x no corpo de mais_um terá o valor Some(5). Em seguida, comparamos isso com cada braço de jogo.

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

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

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

Some(5) corresponder a Some(i)? Sim, é verdade! Temos a mesma variante. O i vincula-se ao valor contido em Some, portanto, i assume o valor 5. O código no braço de correspondência é então executado, então adicionamos 1 ao valor de i e criamos um novo valor Some com nosso total interno 6.

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

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

Corresponde! Não há valor a ser adicionado, então o programa para e retorna o valor None no lado direito de =>. Como o primeiro braço combinou, nenhum outro braço é comparado.

Combinar match e enums é útil em muitas situações. Você verá muito esse padrão no código Rust: match em um enum, vincule uma variável aos dados internos e execute o código com base nele. É um pouco complicado no início, mas quando você se acostumar, vai desejar tê-lo em todas linguagens. É sempre um favorito do usuário.

As partidas são exaustivas

Há um outro aspecto de match que precisamos discutir. Considere esta versão de nossa função mais_um que tem um bug e não compila:

Esse código não compila Esse código não compila.

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

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

Não tratamos do caso None, então este código causará um bug. Felizmente, é um bug que Rust sabe como detectar. Se tentarmos compilar este 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
    |
    = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
    = note: the matched value is of type `Option<i32>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums`

To learn more, run the command again with --verbose.

Rust sabe que não cobrimos todos os casos possíveis e até sabe qual padrão esquecemos! As correspondências em Rust são exaustivas: devemos esgotar todas as possibilidades para que o código seja válido. Especialmente no caso de Option<T>, quando Rust nos impede de esquecer de lidar explicitamente com o caso None, ele nos protege de assumir que temos um valor quando poderíamos ter nulo, tornando assim impossível o erro de bilhões de dólares discutido anteriormente.

O espaço reservado _

Rust também tem um padrão que podemos usar quando não queremos listar todos os valores possíveis. Por exemplo, a u8 pode ter valores válidos de 0 a 255. Se nos preocupamos apenas com os valores 1, 3, 5 e 7, não queremos ter que listar 0, 2, 4, 6, 8, 9 todo o caminho até 255. Felizmente, não precisamos: podemos usar o padrão especial _ em vez disso:

fn main() {
    let algum_valor_u8 = 0u8;
    match algum_valor_u8 {
        1 => println!("um"),
        3 => println!("tres"),
        5 => println!("cinco"),
        7 => println!("sete"),
        _ => (),
    }
}

O padrão _ corresponderá a qualquer valor. Colocando-o após nossos outros braços, o _irá corresponder a todos os casos possíveis que não foram especificados antes dele. O () é apenas o valor da unidade, portanto, nada acontecerá no caso _. Como resultado, podemos dizer que não queremos fazer nada para todos os valores possíveis que não listamos antes do espaço reservado _.

No entanto, a expressão match pode ser um pouco prolixa em uma situação na qual nos importamos com apenas um dos casos. Para esta situação, a Rust fornece if let.

Mais sobre padrões e correspondência podem ser encontrados no capítulo 18.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

0 comentários:

Postar um comentário