Prática de Sintaxe de Padrões Rust

Beginner

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

Introdução

Bem-vindo(a) à Sintaxe de Padrões (Pattern Syntax). Este laboratório faz parte do Livro do Rust. Você pode praticar suas habilidades em Rust no LabEx.

Neste laboratório, discutimos a sintaxe válida em padrões e fornecemos exemplos de quando e por que você pode querer usar cada um.

Sintaxe de Padrões (Pattern Syntax)

Nesta seção, reunimos toda a sintaxe que é válida em padrões e discutimos por que e quando você pode querer usar cada um.

Correspondência de Literais (Matching Literals)

Como você viu no Capítulo 6, você pode corresponder padrões diretamente a literais. O código a seguir fornece alguns exemplos:

Nome do arquivo: src/main.rs

let x = 1;

match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    _ => println!("anything"),
}

Este código imprime one porque o valor em x é 1. Essa sintaxe é útil quando você deseja que seu código execute uma ação se receber um valor concreto específico.

Correspondência de Variáveis Nomeadas (Matching Named Variables)

Variáveis nomeadas são padrões irrefutáveis que correspondem a qualquer valor, e as usamos muitas vezes neste livro. No entanto, há uma complicação quando você usa variáveis nomeadas em expressões match. Como match inicia um novo escopo, as variáveis declaradas como parte de um padrão dentro da expressão match sombrearão aquelas com o mesmo nome fora da construção match, como é o caso com todas as variáveis. Na Listagem 18-11, declaramos uma variável chamada x com o valor Some(5) e uma variável y com o valor 10. Em seguida, criamos uma expressão match no valor x. Observe os padrões nos braços do match e o println! no final, e tente descobrir o que o código imprimirá antes de executar este código ou ler mais.

Nome do arquivo: src/main.rs

fn main() {
  1 let x = Some(5);
  2 let y = 10;

    match x {
      3 Some(50) => println!("Got 50"),
      4 Some(y) => println!("Matched, y = {y}"),
      5 _ => println!("Default case, x = {:?}", x),
    }

  6 println!("at the end: x = {:?}, y = {y}", x);
}

Listagem 18-11: Uma expressão match com um braço que introduz uma variável sombreada y

Vamos analisar o que acontece quando a expressão match é executada. O padrão no primeiro braço do match [3] não corresponde ao valor definido de x [1], então o código continua.

O padrão no segundo braço do match [4] introduz uma nova variável chamada y que corresponderá a qualquer valor dentro de um valor Some. Como estamos em um novo escopo dentro da expressão match, esta é uma nova variável y, não a y que declaramos no início com o valor 10 [2]. Esta nova ligação y corresponderá a qualquer valor dentro de um Some, que é o que temos em x. Portanto, este novo y se liga ao valor interno do Some em x. Esse valor é 5, então a expressão para esse braço é executada e imprime Matched, y = 5.

Se x tivesse sido um valor None em vez de Some(5), os padrões nos dois primeiros braços não teriam correspondido, então o valor teria correspondido ao sublinhado [5]. Não introduzimos a variável x no padrão do braço do sublinhado, então o x na expressão ainda é o x externo que não foi sombreado. Nesse caso hipotético, o match imprimiria Default case, x = None.

Quando a expressão match termina, seu escopo termina, e também o escopo do y interno. O último println! [6] produz at the end: x = Some(5), y = 10.

Para criar uma expressão match que compare os valores de x e y externos, em vez de introduzir uma variável sombreada, precisaríamos usar uma condição de guarda (match guard) em vez disso. Falaremos sobre guardas de correspondência em "Condicionais Extras com Guardas de Correspondência".

Múltiplos Padrões (Multiple Patterns)

Em expressões match, você pode corresponder a múltiplos padrões usando a sintaxe |, que é o operador or (ou) de padrão. Por exemplo, no código a seguir, comparamos o valor de x com os braços do match, o primeiro dos quais tem uma opção or, significando que se o valor de x corresponder a qualquer um dos valores naquele braço, o código desse braço será executado:

Nome do arquivo: src/main.rs

let x = 1;

match x {
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}

Este código imprime one or two.

Correspondência de Intervalos de Valores com ..= (Matching Ranges of Values with ..=)

A sintaxe ..= nos permite corresponder a um intervalo inclusivo de valores. No código a seguir, quando um padrão corresponde a qualquer um dos valores dentro do intervalo fornecido, esse braço será executado:

Nome do arquivo: src/main.rs

let x = 5;

match x {
    1..=5 => println!("one through five"),
    _ => println!("something else"),
}

Se x for 1, 2, 3, 4 ou 5, o primeiro braço corresponderá. Essa sintaxe é mais conveniente para múltiplos valores de correspondência do que usar o operador | para expressar a mesma ideia; se fôssemos usar |, teríamos que especificar 1 | 2 | 3 | 4 | 5. Especificar um intervalo é muito mais curto, especialmente se quisermos corresponder, por exemplo, qualquer número entre 1 e 1.000!

O compilador verifica se o intervalo não está vazio no tempo de compilação, e como os únicos tipos para os quais o Rust pode dizer se um intervalo está vazio ou não são char e valores numéricos, os intervalos são permitidos apenas com valores numéricos ou char.

Aqui está um exemplo usando intervalos de valores char:

Nome do arquivo: src/main.rs

let x = 'c';

match x {
    'a'..='j' => println!("early ASCII letter"),
    'k'..='z' => println!("late ASCII letter"),
    _ => println!("something else"),
}

Rust pode dizer que 'c' está dentro do intervalo do primeiro padrão e imprime early ASCII letter.

Desestruturação para Desmembrar Valores (Destructuring to Break Apart Values)

Também podemos usar padrões para desestruturar structs, enums e tuplas para usar diferentes partes desses valores. Vamos analisar cada valor.

Desestruturando Structs (Destructuring Structs)

A Listagem 18-12 mostra uma struct Point com dois campos, x e y, que podemos desmembrar usando um padrão com uma declaração let.

Nome do arquivo: src/main.rs

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

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}

Listagem 18-12: Desestruturando os campos de uma struct em variáveis separadas

Este código cria as variáveis a e b que correspondem aos valores dos campos x e y da struct p. Este exemplo mostra que os nomes das variáveis no padrão não precisam corresponder aos nomes dos campos da struct. No entanto, é comum corresponder os nomes das variáveis aos nomes dos campos para facilitar a lembrança de quais variáveis vieram de quais campos. Devido a esse uso comum, e porque escrever let Point { x: x, y: y } = p; contém muita duplicação, o Rust tem uma abreviação para padrões que correspondem aos campos da struct: você só precisa listar o nome do campo da struct, e as variáveis criadas a partir do padrão terão os mesmos nomes. A Listagem 18-13 se comporta da mesma maneira que o código na Listagem 18-12, mas as variáveis criadas no padrão let são x e y em vez de a e b.

Nome do arquivo: src/main.rs

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

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}

Listagem 18-13: Desestruturando campos de struct usando a abreviação de campo de struct

Este código cria as variáveis x e y que correspondem aos campos x e y da variável p. O resultado é que as variáveis x e y contêm os valores da struct p.

Também podemos desestruturar com valores literais como parte do padrão da struct, em vez de criar variáveis para todos os campos. Fazer isso nos permite testar alguns dos campos para valores específicos enquanto criamos variáveis para desestruturar os outros campos.

Na Listagem 18-14, temos uma expressão match que separa os valores Point em três casos: pontos que estão diretamente no eixo x (o que é verdade quando y = 0), no eixo y (x = 0), ou em nenhum dos eixos.

Nome do arquivo: src/main.rs

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("On the x axis at {x}"),
        Point { x: 0, y } => println!("On the y axis at {y}"),
        Point { x, y } => {
            println!("On neither axis: ({x}, {y})");
        }
    }
}

Listagem 18-14: Desestruturando e correspondendo valores literais em um padrão

O primeiro braço corresponderá a qualquer ponto que esteja no eixo x especificando que o campo y corresponde se seu valor corresponder ao literal 0. O padrão ainda cria uma variável x que podemos usar no código para este braço.

Da mesma forma, o segundo braço corresponde a qualquer ponto no eixo y especificando que o campo x corresponde se seu valor for 0 e cria uma variável y para o valor do campo y. O terceiro braço não especifica nenhum literal, então ele corresponde a qualquer outro Point e cria variáveis para os campos x e y.

Neste exemplo, o valor p corresponde ao segundo braço em virtude de x conter um 0, então este código imprimirá On the y axis at 7.

Lembre-se que uma expressão match para de verificar os braços assim que encontra o primeiro padrão correspondente, então, embora Point { x: 0, y: 0} esteja no eixo x e no eixo y, este código só imprimirá On the x axis at 0.

Desestruturando Enums (Destructuring Enums)

Nós desestruturamos enums neste livro (por exemplo, na Listagem 6-5), mas ainda não discutimos explicitamente que o padrão para desestruturar um enum corresponde à maneira como os dados armazenados dentro do enum são definidos. Como exemplo, na Listagem 18-15, usamos o enum Message da Listagem 6-2 e escrevemos um match com padrões que desestruturarão cada valor interno.

Nome do arquivo: src/main.rs

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
  1 let msg = Message::ChangeColor(0, 160, 255);

    match msg {
      2 Message::Quit => {
            println!(
                "The Quit variant has no data to destructure."
            );
        }
      3 Message::Move { x, y } => {
            println!(
                "Move in the x dir {x}, in the y dir {y}"
            );
        }
      4 Message::Write(text) => {
            println!("Text message: {text}");
        }
      5 Message::ChangeColor(r, g, b) => println!(
            "Change color to red {r}, green {g}, and blue {b}"
        ),
    }
}

Listagem 18-15: Desestruturando variantes de enum que contêm diferentes tipos de valores

Este código imprimirá Change color to red 0, green 160, and blue 255. Tente alterar o valor de msg [1] para ver o código dos outros braços sendo executado.

Para variantes de enum sem nenhum dado, como Message::Quit [2], não podemos desestruturar o valor ainda mais. Só podemos corresponder ao valor literal Message::Quit, e nenhuma variável está nesse padrão.

Para variantes de enum semelhantes a structs, como Message::Move [3], podemos usar um padrão semelhante ao padrão que especificamos para corresponder a structs. Após o nome da variante, colocamos chaves e, em seguida, listamos os campos com variáveis para quebrar as partes para usar no código para este braço. Aqui, usamos a forma abreviada como fizemos na Listagem 18-13.

Para variantes de enum semelhantes a tuplas, como Message::Write que contém uma tupla com um elemento [4] e Message::ChangeColor que contém uma tupla com três elementos [5], o padrão é semelhante ao padrão que especificamos para corresponder a tuplas. O número de variáveis no padrão deve corresponder ao número de elementos na variante que estamos correspondendo.

Desestruturando Structs e Enums Aninhados

Até agora, nossos exemplos têm correspondido a structs ou enums em um nível de profundidade, mas a correspondência pode funcionar em itens aninhados também! Por exemplo, podemos refatorar o código na Listagem 18-15 para suportar cores RGB e HSV na mensagem ChangeColor, conforme mostrado na Listagem 18-16.

Nome do arquivo: src/main.rs

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => println!(
            "Change color to red {r}, green {g}, and blue {b}"
        ),
        Message::ChangeColor(Color::Hsv(h, s, v)) => println!(
            "Change color to hue {h}, saturation {s}, value {v}"
        ),
        _ => (),
    }
}

Listagem 18-16: Correspondência em enums aninhados

O padrão do primeiro braço na expressão match corresponde a uma variante de enum Message::ChangeColor que contém uma variante Color::Rgb; então o padrão se vincula aos três valores i32 internos. O padrão do segundo braço também corresponde a uma variante de enum Message::ChangeColor, mas o enum interno corresponde a Color::Hsv em vez disso. Podemos especificar essas condições complexas em uma expressão match, mesmo que dois enums estejam envolvidos.

Desestruturando Structs e Tuplas

Podemos misturar, combinar e aninhar padrões de desestruturação de maneiras ainda mais complexas. O exemplo a seguir mostra uma desestruturação complicada onde aninhamos structs e tuplas dentro de uma tupla e desestruturamos todos os valores primitivos:

let ((feet, inches), Point { x, y }) =
    ((3, 10), Point { x: 3, y: -10 });

Este código nos permite quebrar tipos complexos em suas partes componentes para que possamos usar os valores nos quais estamos interessados separadamente.

Desestruturar com padrões é uma maneira conveniente de usar partes de valores, como o valor de cada campo em uma struct, separadamente um do outro.

Ignorando Valores em um Padrão

Você viu que, às vezes, é útil ignorar valores em um padrão, como no último braço de um match, para obter um "catchall" (pega-tudo) que, na verdade, não faz nada, mas considera todos os valores restantes possíveis. Existem algumas maneiras de ignorar valores inteiros ou partes de valores em um padrão: usando o padrão _ (que você já viu), usando o padrão _ dentro de outro padrão, usando um nome que começa com um sublinhado ou usando .. para ignorar as partes restantes de um valor. Vamos explorar como e por que usar cada um desses padrões.

Um Valor Inteiro com _

Usamos o sublinhado como um padrão curinga (wildcard) que corresponderá a qualquer valor, mas não se vinculará ao valor. Isso é especialmente útil como o último braço em uma expressão match, mas também podemos usá-lo em qualquer padrão, incluindo parâmetros de função, como mostrado na Listagem 18-17.

Nome do arquivo: src/main.rs

fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {y}");
}

fn main() {
    foo(3, 4);
}

Listagem 18-17: Usando _ em uma assinatura de função

Este código ignorará completamente o valor 3 passado como o primeiro argumento e imprimirá This code only uses the y parameter: 4.

Na maioria dos casos, quando você não precisa mais de um determinado parâmetro de função, você alteraria a assinatura para que ela não inclua o parâmetro não utilizado. Ignorar um parâmetro de função pode ser especialmente útil em casos em que, por exemplo, você está implementando uma trait quando precisa de uma determinada assinatura de tipo, mas o corpo da função em sua implementação não precisa de um dos parâmetros. Você então evita obter um aviso do compilador sobre parâmetros de função não utilizados, como faria se usasse um nome em vez disso.

Partes de um Valor com um _ Aninhado

Também podemos usar _ dentro de outro padrão para ignorar apenas parte de um valor, por exemplo, quando queremos testar apenas parte de um valor, mas não temos uso para as outras partes no código correspondente que queremos executar. A Listagem 18-18 mostra o código responsável por gerenciar o valor de uma configuração. Os requisitos de negócios são que o usuário não deve ter permissão para substituir uma personalização existente de uma configuração, mas pode desativar a configuração e dar-lhe um valor se ela estiver atualmente desativada.

Nome do arquivo: src/main.rs

let mut setting_value = Some(5);
let new_setting_value = Some(10);

match (setting_value, new_setting_value) {
    (Some(_), Some(_)) => {
        println!("Can't overwrite an existing customized value");
    }
    _ => {
        setting_value = new_setting_value;
    }
}

println!("setting is {:?}", setting_value);

Listagem 18-18: Usando um sublinhado dentro de padrões que correspondem a variantes Some quando não precisamos usar o valor dentro do Some

Este código imprimirá Can't overwrite an existing customized value e, em seguida, setting is Some(5). No primeiro braço do match, não precisamos corresponder ou usar os valores dentro de nenhuma variante Some, mas precisamos testar o caso em que setting_value e new_setting_value são a variante Some. Nesse caso, imprimimos a razão para não alterar setting_value, e ele não é alterado.

Em todos os outros casos (se setting_value ou new_setting_value for None) expressos pelo padrão _ no segundo braço, queremos permitir que new_setting_value se torne setting_value.

Também podemos usar sublinhados em vários lugares dentro de um padrão para ignorar valores específicos. A Listagem 18-19 mostra um exemplo de ignorar o segundo e o quarto valores em uma tupla de cinco itens.

Nome do arquivo: src/main.rs

let numbers = (2, 4, 8, 16, 32);

match numbers {
    (first, _, third, _, fifth) => {
        println!("Some numbers: {first}, {third}, {fifth}");
    }
}

Listagem 18-19: Ignorando várias partes de uma tupla

Este código imprimirá Some numbers: 2, 8, 32, e os valores 4 e 16 serão ignorados.

Uma Variável Não Utilizada Começando Seu Nome com _

Se você criar uma variável, mas não usá-la em nenhum lugar, o Rust geralmente emitirá um aviso porque uma variável não utilizada pode ser um bug. No entanto, às vezes é útil poder criar uma variável que você não usará ainda, como quando você está prototipando ou apenas começando um projeto. Nessa situação, você pode dizer ao Rust para não avisá-lo sobre a variável não utilizada, começando o nome da variável com um sublinhado. Na Listagem 18-20, criamos duas variáveis não utilizadas, mas quando compilamos este código, devemos receber apenas um aviso sobre uma delas.

Nome do arquivo: src/main.rs

fn main() {
    let _x = 5;
    let y = 10;
}

Listagem 18-20: Começando o nome de uma variável com um sublinhado para evitar receber avisos de variável não utilizada

Aqui, recebemos um aviso sobre não usar a variável y, mas não recebemos um aviso sobre não usar _x.

Observe que há uma diferença sutil entre usar apenas _ e usar um nome que começa com um sublinhado. A sintaxe _x ainda vincula o valor à variável, enquanto _ não vincula de forma alguma. Para mostrar um caso em que essa distinção importa, a Listagem 18-21 nos fornecerá um erro.

Nome do arquivo: src/main.rs

let s = Some(String::from("Hello!"));

if let Some(_s) = s {
    println!("found a string");
}

println!("{:?}", s);

Listagem 18-21: Uma variável não utilizada começando com um sublinhado ainda vincula o valor, o que pode assumir a propriedade do valor.

Receberemos um erro porque o valor s ainda será movido para _s, o que nos impede de usar s novamente. No entanto, usar o sublinhado por si só nunca se vincula ao valor. A Listagem 18-22 compilará sem nenhum erro porque s não é movido para _.

Nome do arquivo: src/main.rs

let s = Some(String::from("Hello!"));

if let Some(_) = s {
    println!("found a string");
}

println!("{:?}", s);

Listagem 18-22: Usar um sublinhado não vincula o valor.

Este código funciona perfeitamente porque nunca vinculamos s a nada; ele não é movido.

Partes Restantes de um Valor com ..

Com valores que têm muitas partes, podemos usar a sintaxe .. para usar partes específicas e ignorar o restante, evitando a necessidade de listar sublinhados para cada valor ignorado. O padrão .. ignora quaisquer partes de um valor que não tenhamos correspondido explicitamente no restante do padrão. Na Listagem 18-23, temos uma struct Point que contém uma coordenada no espaço tridimensional. Na expressão match, queremos operar apenas na coordenada x e ignorar os valores nos campos y e z.

Nome do arquivo: src/main.rs

struct Point {
    x: i32,
    y: i32,
    z: i32,
}

let origin = Point { x: 0, y: 0, z: 0 };

match origin {
    Point { x, .. } => println!("x is {x}"),
}

Listagem 18-23: Ignorando todos os campos de um Point exceto x usando ..

Listamos o valor x e, em seguida, apenas incluímos o padrão ... Isso é mais rápido do que ter que listar y: _ e z: _, particularmente quando estamos trabalhando com structs que têm muitos campos em situações em que apenas um ou dois campos são relevantes.

A sintaxe .. se expandirá para quantos valores forem necessários. A Listagem 18-24 mostra como usar .. com uma tupla.

Nome do arquivo: src/main.rs

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first, .., last) => {
            println!("Some numbers: {first}, {last}");
        }
    }
}

Listagem 18-24: Correspondendo apenas aos primeiros e últimos valores em uma tupla e ignorando todos os outros valores

Neste código, os primeiros e últimos valores são correspondidos com first e last. O .. corresponderá e ignorará tudo no meio.

No entanto, usar .. deve ser inequívoco. Se não estiver claro quais valores se destinam à correspondência e quais devem ser ignorados, o Rust nos dará um erro. A Listagem 18-25 mostra um exemplo de uso ambíguo de .., portanto, não compilará.

Nome do arquivo: src/main.rs

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {second}");
        },
    }
}

Listagem 18-25: Uma tentativa de usar .. de forma ambígua

Quando compilamos este exemplo, recebemos este erro:

error: `..` can only be used once per tuple pattern
 --> src/main.rs:5:22
  |
5 |         (.., second, ..) => {
  |          --          ^^ can only be used once per tuple pattern
  |          |
  |          previously used here

É impossível para o Rust determinar quantos valores na tupla ignorar antes de corresponder a um valor com second e, em seguida, quantos valores adicionais ignorar depois disso. Este código pode significar que queremos ignorar 2, vincular second a 4 e, em seguida, ignorar 8, 16 e 32; ou que queremos ignorar 2 e 4, vincular second a 8 e, em seguida, ignorar 16 e 32; e assim por diante. O nome da variável second não significa nada de especial para o Rust, então recebemos um erro do compilador porque usar .. em dois lugares como este é ambíguo.

Condicionais Extras com Match Guards

Um match guard é uma condição if adicional, especificada após o padrão em um braço match, que também deve corresponder para que esse braço seja escolhido. Match guards são úteis para expressar ideias mais complexas do que um padrão sozinho permite.

A condição pode usar variáveis criadas no padrão. A Listagem 18-26 mostra um match onde o primeiro braço tem o padrão Some(x) e também tem um match guard de if x % 2 == 0 (que será true se o número for par).

Nome do arquivo: src/main.rs

let num = Some(4);

match num {
    Some(x) if x % 2 == 0 => println!("The number {x} is even"),
    Some(x) => println!("The number {x} is odd"),
    None => (),
}

Listagem 18-26: Adicionando um match guard a um padrão

Este exemplo imprimirá The number 4 is even. Quando num é comparado ao padrão no primeiro braço, ele corresponde porque Some(4) corresponde a Some(x). Então, o match guard verifica se o resto da divisão de x por 2 é igual a 0 e, como é, o primeiro braço é selecionado.

Se num fosse Some(5) em vez disso, o match guard no primeiro braço teria sido false porque o resto de 5 dividido por 2 é 1, que não é igual a 0. Rust então iria para o segundo braço, que corresponderia porque o segundo braço não tem um match guard e, portanto, corresponde a qualquer variante Some.

Não há como expressar a condição if x % 2 == 0 dentro de um padrão, então o match guard nos dá a capacidade de expressar essa lógica. A desvantagem dessa expressividade adicional é que o compilador não tenta verificar a exaustividade quando expressões de match guard estão envolvidas.

Na Listagem 18-11, mencionamos que poderíamos usar match guards para resolver nosso problema de sombreamento de padrões. Lembre-se de que criamos uma nova variável dentro do padrão na expressão match em vez de usar a variável fora do match. Essa nova variável significava que não podíamos testar em relação ao valor da variável externa. A Listagem 18-27 mostra como podemos usar um match guard para corrigir esse problema.

Nome do arquivo: src/main.rs

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {n}"),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {y}", x);
}

Listagem 18-27: Usando um match guard para testar a igualdade com uma variável externa

Este código agora imprimirá Default case, x = Some(5). O padrão no segundo braço match não introduz uma nova variável y que sombrearia o y externo, o que significa que podemos usar o y externo no match guard. Em vez de especificar o padrão como Some(y), que teria sombreado o y externo, especificamos Some(n). Isso cria uma nova variável n que não sombreia nada porque não há nenhuma variável n fora do match.

O match guard if n == y não é um padrão e, portanto, não introduz novas variáveis. Este y é o y externo, em vez de um novo y sombreado, e podemos procurar um valor que tenha o mesmo valor que o y externo, comparando n a y.

Você também pode usar o operador or | em um match guard para especificar vários padrões; a condição do match guard se aplicará a todos os padrões. A Listagem 18-28 mostra a precedência ao combinar um padrão que usa | com um match guard. A parte importante deste exemplo é que o match guard if y se aplica a 4, 5, e 6, embora possa parecer que if y se aplica apenas a 6.

Nome do arquivo: src/main.rs

let x = 4;
let y = false;

match x {
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"),
}

Listagem 18-28: Combinando vários padrões com um match guard

A condição match afirma que o braço corresponde apenas se o valor de x for igual a 4, 5 ou 6 e se y for true. Quando este código é executado, o padrão do primeiro braço corresponde porque x é 4, mas o match guard if y é false, então o primeiro braço não é escolhido. O código passa para o segundo braço, que corresponde, e este programa imprime no. A razão é que a condição if se aplica a todo o padrão 4 | 5 | 6, não apenas ao último valor 6. Em outras palavras, a precedência de um match guard em relação a um padrão se comporta assim:

(4 | 5 | 6) if y => ...

em vez disso:

4 | 5 | (6 if y) => ...

Após executar o código, o comportamento de precedência é evidente: se o match guard fosse aplicado apenas ao valor final na lista de valores especificados usando o operador |, o braço teria correspondido e o programa teria impresso yes.

@ Bindings

O operador at @ nos permite criar uma variável que contém um valor ao mesmo tempo em que testamos esse valor para uma correspondência de padrão. Na Listagem 18-29, queremos testar se um campo id de Message::Hello está dentro do intervalo 3..=7. Também queremos vincular o valor à variável id_variable para que possamos usá-lo no código associado ao braço. Poderíamos nomear essa variável id, a mesma do campo, mas para este exemplo usaremos um nome diferente.

Nome do arquivo: src/main.rs

enum Message {
    Hello { id: i32 },
}

let msg = Message::Hello { id: 5 };

match msg {
    Message::Hello {
        id: id_variable @ 3..=7,
    } => println!("Found an id in range: {id_variable}"),
    Message::Hello { id: 10..=12 } => {
        println!("Found an id in another range")
    }
    Message::Hello { id } => println!("Some other id: {id}"),
}

Listagem 18-29: Usando @ para vincular a um valor em um padrão enquanto também o testa

Este exemplo imprimirá Found an id in range: 5. Ao especificar id_variable @ antes do intervalo 3..=7, estamos capturando qualquer valor que corresponda ao intervalo, ao mesmo tempo em que testamos se o valor corresponde ao padrão do intervalo.

No segundo braço, onde temos apenas um intervalo especificado no padrão, o código associado ao braço não tem uma variável que contenha o valor real do campo id. O valor do campo id poderia ter sido 10, 11 ou 12, mas o código que acompanha esse padrão não sabe qual é. O código do padrão não é capaz de usar o valor do campo id porque não salvamos o valor id em uma variável.

No último braço, onde especificamos uma variável sem um intervalo, temos o valor disponível para usar no código do braço em uma variável chamada id. A razão é que usamos a sintaxe abreviada do campo da struct. Mas não aplicamos nenhum teste ao valor no campo id neste braço, como fizemos com os dois primeiros braços: qualquer valor corresponderia a este padrão.

Usar @ nos permite testar um valor e salvá-lo em uma variável dentro de um padrão.

Resumo

Parabéns! Você concluiu o laboratório de Sintaxe de Padrões. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.