Практика синтаксиса шаблонов в Rust

RustRustBeginner
Практиковаться сейчас

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

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в раздел Синтаксис шаблонов (Pattern Syntax). Этот практикум является частью Книги по Rust. Вы можете практиковать свои навыки программирования на Rust в LabEx.

В этом практикуме мы рассмотрим допустимый синтаксис шаблонов и приведем примеры случаев, когда и почему вы можете захотеть использовать каждый из них.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/MemorySafetyandManagementGroup(["Memory Safety and Management"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/boolean_type("Boolean Type") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/MemorySafetyandManagementGroup -.-> rust/lifetime_specifiers("Lifetime Specifiers") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100446{{"Практика синтаксиса шаблонов в Rust"}} rust/mutable_variables -.-> lab-100446{{"Практика синтаксиса шаблонов в Rust"}} rust/integer_types -.-> lab-100446{{"Практика синтаксиса шаблонов в Rust"}} rust/boolean_type -.-> lab-100446{{"Практика синтаксиса шаблонов в Rust"}} rust/string_type -.-> lab-100446{{"Практика синтаксиса шаблонов в Rust"}} rust/function_syntax -.-> lab-100446{{"Практика синтаксиса шаблонов в Rust"}} rust/expressions_statements -.-> lab-100446{{"Практика синтаксиса шаблонов в Rust"}} rust/lifetime_specifiers -.-> lab-100446{{"Практика синтаксиса шаблонов в Rust"}} rust/method_syntax -.-> lab-100446{{"Практика синтаксиса шаблонов в Rust"}} end

Синтаксис шаблонов (Pattern Syntax)

В этом разделе мы соберем весь допустимый синтаксис шаблонов и обсудим, почему и когда вы можете захотеть использовать каждый из них.

Сопоставление с литералами (Matching Literals)

Как вы видели в главе 6, вы можете напрямую сопоставлять шаблоны с литералами. В следующем коде приведены некоторые примеры:

Имя файла: src/main.rs

let x = 1;

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

Этот код выводит one, потому что значение в x равно 1. Этот синтаксис полезен, когда вы хотите, чтобы ваш код выполнял какое - то действие при получении определенного конкретного значения.

Сопоставление с именованными переменными (Matching Named Variables)

Именованные переменные представляют собой непреложные шаблоны (irrefutable patterns), которые совпадают с любым значением, и мы использовали их много раз в этой книге. Однако возникает сложность, когда вы используете именованные переменные в выражениях match. Поскольку match начинает новую область видимости, переменные, объявленные как часть шаблона внутри выражения match, будут скрывать переменные с тем же именем вне конструкции match, как и все переменные. В листинге 18 - 11 мы объявляем переменную с именем x со значением Some(5) и переменную y со значением 10. Затем мы создаем выражение match для значения x. Посмотрите на шаблоны в ветках match и на вызов println! в конце, и попробуйте понять, что напечатает код, прежде чем запускать его или продолжать чтение.

Имя файла: 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);
}

Листинг 18 - 11: Выражение match с веткой, которая вводит скрывающую переменную y

Давайте разберем, что происходит, когда выполняется выражение match. Шаблон в первой ветке match [3] не совпадает с определенным значением x [1], поэтому выполнение кода продолжается.

Шаблон во второй ветке match [4] вводит новую переменную с именем y, которая будет совпадать с любым значением внутри значения Some. Поскольку мы находимся в новой области видимости внутри выражения match, это новая переменная y, а не та y, которую мы объявили в начале со значением 10 [2]. Эта новая привязка y будет совпадать с любым значением внутри Some, которое у нас есть в x. Поэтому эта новая y связывается с внутренним значением Some в x. Это значение равно 5, поэтому выражение для этой ветки выполняется и выводит Matched, y = 5.

Если бы x было значением None вместо Some(5), шаблоны в первых двух ветках не совпали бы, поэтому значение совпадало бы с подчеркиванием [5]. Мы не ввели переменную x в шаблоне ветки с подчеркиванием, поэтому x в выражении по-прежнему является внешней переменной x, которая не была скрыта. В этом гипотетическом случае match напечатал бы Default case, x = None.

Когда выполнение выражения match завершено, его область видимости заканчивается, и заканчивается область видимости внутренней переменной y. Последний вызов println! [6] выводит at the end: x = Some(5), y = 10.

Чтобы создать выражение match, которое сравнивает значения внешних переменных x и y, вместо введения скрывающей переменной, нам нужно будет использовать условный оператор - охранник (match guard). Мы поговорим об охранниках match в разделе "Дополнительные условия с помощью охранников match (Extra Conditionals with Match Guards)".

Несколько шаблонов (Multiple Patterns)

В выражениях match вы можете сопоставить несколько шаблонов, используя синтаксис |, который представляет собой оператор "или" (or) для шаблонов. Например, в следующем коде мы сопоставляем значение x с ветками match, первая из которых имеет вариант "или", то есть если значение x совпадает с любым из значений в этой ветке, будет выполнен код этой ветки:

Имя файла: src/main.rs

let x = 1;

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

Этот код выводит one or two.

Сопоставление диапазонов значений с помощью ..=

Синтаксис ..= позволяет нам сопоставить инклюзивный диапазон значений. В следующем коде, когда шаблон совпадает с любым из значений в заданном диапазоне, будет выполнена соответствующая ветка:

Имя файла: src/main.rs

let x = 5;

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

Если x равно 1, 2, 3, 4 или 5, то будет совпадать первая ветка. Этот синтаксис более удобен для множества значений сопоставления, чем использование оператора | для выражения той же идеи; если бы мы использовали |, нам пришлось бы указать 1 | 2 | 3 | 4 | 5. Указание диапазона намного короче, особенно если мы хотим сопоставить, скажем, любое число от 1 до 1000!

Компилятор проверяет, что диапазон не пуст на этапе компиляции, и так как только для типов char и числовых значений Rust может определить, пуст ли диапазон или нет, диапазоны разрешены только для числовых значений или значений типа char.

Вот пример использования диапазонов значений типа char:

Имя файла: src/main.rs

let x = 'c';

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

Rust определяет, что 'c' находится в диапазоне первого шаблона и выводит early ASCII letter.

Деструктуризация для разделения значений (Destructuring to Break Apart Values)

Мы также можем использовать шаблоны для деструктуризации структур (structs), перечислений (enums) и кортежей (tuples), чтобы использовать различные части этих значений. Давайте рассмотрим каждый тип значения.

Деструктуризация структур (Destructuring Structs)

В листинге 18 - 12 показана структура Point с двумя полями, x и y, которые мы можем разделить с использованием шаблона в инструкции let.

Имя файла: 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);
}

Листинг 18 - 12: Деструктуризация полей структуры в отдельные переменные

Этот код создает переменные a и b, которые соответствуют значениям полей x и y структуры p. В этом примере показано, что имена переменных в шаблоне не обязательно должны совпадать с именами полей структуры. Однако обычно имена переменных совпадают с именами полей, чтобы было легче запомнить, откуда взялись переменные. Из - за такой распространенной практики и из - за того, что запись let Point { x: x, y: y } = p; содержит много дублирования, в Rust есть сокращенная запись для шаблонов, которые соответствуют полям структуры: вам нужно только перечислить имена полей структуры, и переменные, созданные из шаблона, будут иметь те же имена. Листинг 18 - 13 работает так же, как код в листинге 18 - 12, но переменные, созданные в шаблоне let, - это x и y, а не a и b.

Имя файла: 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);
}

Листинг 18 - 13: Деструктуризация полей структуры с использованием сокращенной записи для полей структуры

Этот код создает переменные x и y, которые соответствуют полям x и y переменной p. В результате переменные x и y содержат значения из структуры p.

Мы также можем деструктурировать, используя литеральные значения в качестве части шаблона структуры, вместо создания переменных для всех полей. Это позволяет нам проверить некоторые поля на соответствие определенным значениям, в то время как создаются переменные для деструктуризации других полей.

В листинге 18 - 14 у нас есть выражение match, которое разделяет значения Point на три случая: точки, лежащие непосредственно на оси x (это верно, когда y = 0), на оси y (x = 0) или не на какой - либо из осей.

Имя файла: 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})");
        }
    }
}

Листинг 18 - 14: Деструктуризация и сопоставление литеральных значений в одном шаблоне

Первая ветка будет совпадать с любой точкой, лежащей на оси x, указав, что поле y совпадает, если его значение совпадает с литералом 0. Шаблон все еще создает переменную x, которую мы можем использовать в коде этой ветки.

Аналогично, вторая ветка совпадает с любой точкой на оси y, указав, что поле x совпадает, если его значение равно 0, и создает переменную y для значения поля y. Третья ветка не указывает никаких литералов, поэтому она совпадает с любой другой точкой Point и создает переменные для полей x и y.

В этом примере значение p совпадает со второй веткой, так как x содержит 0, поэтому этот код напечатает On the y axis at 7.

Помните, что выражение match прекращает проверку веток, как только оно находит первый совпадающий шаблон, поэтому даже если Point { x: 0, y: 0} лежит на оси x и на оси y, этот код напечатает только On the x axis at 0.

Деструктуризация перечислений (Destructuring Enums)

Мы уже деструктурировали перечисления в этой книге (например, в листинге 6 - 5), но еще не разбирали явно, что шаблон для деструктуризации перечисления соответствует способу определения данных, хранящихся в перечислении. В качестве примера, в листинге 18 - 15 мы используем перечисление Message из листинга 6 - 2 и пишем выражение match с шаблонами, которые будут деструктурировать каждое внутреннее значение.

Имя файла: 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}"
        ),
    }
}

Листинг 18 - 15: Деструктуризация вариантов перечисления, которые хранят разные типы значений

Этот код напечатает Change color to red 0, green 160, and blue 255. Попробуйте изменить значение msg [1], чтобы увидеть выполнение кода других веток.

Для вариантов перечисления без каких - либо данных, таких как Message::Quit [2], мы не можем деструктурировать значение дальше. Мы можем только сопоставить литеральное значение Message::Quit, и в этом шаблоне нет переменных.

Для вариантов перечисления, похожих на структуры, таких как Message::Move [3], мы можем использовать шаблон, похожий на тот, который мы задаем для сопоставления структур. После имени варианта мы ставим фигурные скобки и затем перечисляем поля с переменными, чтобы разделить части и использовать их в коде этой ветки. Здесь мы используем сокращенную форму, как в листинге 18 - 13.

Для вариантов перечисления, похожих на кортежи, таких как Message::Write, который хранит кортеж с одним элементом [4], и Message::ChangeColor, который хранит кортеж с тремя элементами [5], шаблон похож на тот, который мы задаем для сопоставления кортежей. Количество переменных в шаблоне должно совпадать с количеством элементов в варианте, с которым мы сопоставляем.

Деструктуризация вложенных структур и перечислений (Destructuring Nested Structs and Enums)

До сих пор наши примеры были связаны с сопоставлением структур или перечислений на одном уровне, но сопоставление может работать и с вложенными элементами! Например, мы можем переписать код из листинга 18 - 15 так, чтобы он поддерживал цвета в формате RGB и HSV в сообщении ChangeColor, как показано в листинге 18 - 16.

Имя файла: 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}"
        ),
        _ => (),
    }
}

Листинг 18 - 16: Сопоставление вложенных перечислений

Шаблон первой ветки в выражении match соответствует варианту перечисления Message::ChangeColor, который содержит вариант Color::Rgb; затем шаблон связывается с тремя внутренними значениями типа i32. Шаблон второй ветки также соответствует варианту перечисления Message::ChangeColor, но внутреннее перечисление соответствует Color::Hsv. Мы можем задать эти сложные условия в одном выражении match, даже если участвуют два перечисления.

Деструктуризация структур и кортежей (Destructuring Structs and Tuples)

Мы можем комбинировать, сопоставлять и вкладывать шаблоны деструктуризации еще более сложными способами. В следующем примере показана сложная деструктуризация, где мы вкладываем структуры и кортежи в кортеж и извлекаем все примитивные значения:

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

Этот код позволяет нам разбить сложные типы на составляющие части, чтобы мы могли отдельно использовать значения, которые нас интересуют.

Деструктуризация с использованием шаблонов - это удобный способ использовать части значений, например, значения каждого поля в структуре, отдельно друг от друга.

Игнорирование значений в шаблоне (Ignoring Values in a Pattern)

Вы уже видели, что иногда полезно игнорировать значения в шаблоне, например, в последней ветке выражения match, чтобы получить универсальную ветку, которая на самом деле ничего не делает, но учитывает все оставшиеся возможные значения. Существует несколько способов игнорировать целые значения или части значений в шаблоне: использование шаблона _ (который вы уже видели), использование шаблона _ внутри другого шаблона, использование имени, начинающегося с подчеркивания, или использование .. для игнорирования оставшихся частей значения. Давайте рассмотрим, как и почему использовать каждый из этих шаблонов.

Игнорирование целого значения с использованием _

Мы использовали подчеркивание в качестве шаблона - подстановочного знака, который совпадает с любым значением, но не связывается с ним. Это особенно полезно в последней ветке выражения match, но мы также можем использовать его в любом шаблоне, включая параметры функций, как показано в листинге 18 - 17.

Имя файла: src/main.rs

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

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

Листинг 18 - 17: Использование _ в сигнатуре функции

Этот код полностью проигнорирует значение 3, переданное в качестве первого аргумента, и напечатает This code only uses the y parameter: 4.

В большинстве случаев, когда вам больше не нужен определенный параметр функции, вы измените сигнатуру так, чтобы она не включала неиспользуемый параметр. Игнорирование параметра функции может быть особенно полезно, например, когда вы реализуете трейт и вам нужна определенная сигнатура типа, но в теле функции вашей реализации не нужен один из параметров. Таким образом, вы избавляетесь от предупреждения компилятора о неиспользуемых параметрах функции, которое бы появилось, если бы вы использовали имя вместо подчеркивания.

Игнорирование частей значения с использованием вложенного _

Мы также можем использовать _ внутри другого шаблона, чтобы игнорировать только часть значения. Например, когда мы хотим проверить только часть значения, но не используем другие части в соответствующем коде, который мы хотим выполнить. Листинг 18 - 18 показывает код, ответственный за управление значением настройки. Бизнес - требования заключаются в том, что пользователю не должно быть разрешено перезаписывать существующую настройку, но он может сбросить настройку и задать ей новое значение, если она в настоящее время не установлена.

Имя файла: 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);

Листинг 18 - 18: Использование подчеркивания в шаблонах, которые совпадают с вариантами Some, когда нам не нужно использовать значение внутри Some

Этот код напечатает Can't overwrite an existing customized value, а затем setting is Some(5). В первой ветке match нам не нужно совпадать с значениями внутри вариантов Some или использовать их, но нам нужно проверить случай, когда и setting_value, и new_setting_value являются вариантом Some. В этом случае мы выводим причину, по которой не изменяем setting_value, и оно не меняется.

Во всех других случаях (если либо setting_value, либо new_setting_value равно None), которые выражены шаблоном _ во второй ветке, мы хотим разрешить new_setting_value стать setting_value.

Мы также можем использовать подчеркивания в нескольких местах внутри одного шаблона, чтобы игнорировать определенные значения. Листинг 18 - 19 показывает пример игнорирования второго и четвертого значений в кортеже из пяти элементов.

Имя файла: src/main.rs

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

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

Листинг 18 - 19: Игнорирование нескольких частей кортежа

Этот код напечатает Some numbers: 2, 8, 32, и значения 4 и 16 будут проигнорированы.

Неиспользуемая переменная с именем, начинающимся с _

Если вы создаете переменную, но не используете ее нигде, Rust обычно выдаст предупреждение, так как неиспользуемая переменная может быть ошибкой. Однако иногда полезно иметь возможность создать переменную, которую вы еще не будете использовать, например, при разработке прототипа или начале проекта. В такой ситуации вы можете сказать Rust не предупреждать вас о неиспользуемой переменной, начиная имя переменной с подчеркивания. В листинге 18 - 20 мы создаем две неиспользуемые переменные, но при компиляции этого кода мы должны получить предупреждение только о одной из них.

Имя файла: src/main.rs

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

Листинг 18 - 20: Начало имени переменной с подчеркивания, чтобы избежать предупреждений о неиспользуемых переменных

Здесь мы получаем предупреждение о том, что не используем переменную y, но не получаем предупреждения о неиспользовании _x.

Обратите внимание, что есть тонкая разница между использованием только _ и использованием имени, начинающегося с подчеркивания. Синтаксис _x все еще связывает значение с переменной, в то время как _ вообще не связывает. Чтобы показать случай, когда эта разница имеет значение, листинг 18 - 21 выдаст ошибку.

Имя файла: src/main.rs

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

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

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

Листинг 18 - 21: Неиспользуемая переменная, имя которой начинается с подчеркивания, все еще связывает значение, которое может взять владение значением.

Мы получим ошибку, потому что значение s все еще будет перемещено в _s, что мешает нам использовать s снова. Однако использование только подчеркивания никогда не связывает значение. Листинг 18 - 22 скомпилируется без ошибок, потому что s не перемещается в _.

Имя файла: src/main.rs

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

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

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

Листинг 18 - 22: Использование подчеркивания не связывает значение.

Этот код работает нормально, потому что мы никогда не связываем s с чем - то; оно не перемещается.

Игнорирование оставшихся частей значения с использованием ..

Для значений, состоящих из многих частей, мы можем использовать синтаксис .., чтобы использовать определенные части и игнорировать остальные, избавившись от необходимости перечислять подчеркивания для каждого игнорируемого значения. Шаблон .. игнорирует любые части значения, которые мы не явно сопоставили в остальной части шаблона. В листинге 18 - 23 у нас есть структура Point, которая представляет координату в трехмерном пространстве. В выражении match мы хотим работать только с координатой x и игнорировать значения полей y и z.

Имя файла: 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}"),
}

Листинг 18 - 23: Игнорирование всех полей структуры Point, кроме x, с использованием ..

Мы перечисляем значение x, а затем добавляем шаблон ... Это быстрее, чем перечислять y: _ и z: _, особенно когда мы работаем со структурами, имеющими много полей, и только одно или два поля имеют значение.

Синтаксис .. будет охватывать столько значений, сколько необходимо. Листинг 18 - 24 показывает, как использовать .. с кортежем.

Имя файла: src/main.rs

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

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

Листинг 18 - 24: Сопоставление только первого и последнего значений в кортеже и игнорирование всех остальных значений

В этом коде первое и последнее значения сопоставляются с first и last. Шаблон .. сопоставит и проигнорирует все значения в середине.

Однако использование .. должно быть однозначным. Если неясно, какие значения должны быть сопоставлены, а какие игнорированы, Rust выдаст ошибку. Листинг 18 - 25 показывает пример неоднозначного использования .., поэтому этот код не скомпилируется.

Имя файла: src/main.rs

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

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

Листинг 18 - 25: Попытка использовать .. неоднозначно

При компиляции этого примера мы получим такую ошибку:

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

Rust не может определить, сколько значений в кортеже нужно игнорировать перед тем, как сопоставить значение с second, и сколько значений нужно игнорировать после этого. Этот код может означать, что мы хотим игнорировать 2, связать second с 4, а затем игнорировать 8, 16 и 32; или что мы хотим игнорировать 2 и 4, связать second с 8, а затем игнорировать 16 и 32 и так далее. Имя переменной second не имеет особого значения для Rust, поэтому мы получаем ошибку компилятора, так как использование .. в двух местах таким образом является неоднозначным.

Дополнительные условия с использованием защитных выражений в match

Защитное выражение в match (match guard) — это дополнительное условие if, указанное после шаблона в ветке match, которое также должно быть выполнено, чтобы эта ветка была выбрана. Защитные выражения в match полезны для выражения более сложных идей, чем позволяет только шаблон.

Условие может использовать переменные, созданные в шаблоне. Листинг 18 - 26 показывает выражение match, где первая ветка имеет шаблон Some(x) и также содержит защитное выражение if x % 2 == 0 (которое будет true, если число четное).

Имя файла: 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 => (),
}

Листинг 18 - 26: Добавление защитного выражения в шаблон

В этом примере будет напечатано The number 4 is even. Когда num сравнивается с шаблоном в первой ветке, они совпадают, так как Some(4) соответствует Some(x). Затем защитное выражение проверяет, равно ли остаток от деления x на 2 нулю, и так как это так, выбирается первая ветка.

Если бы num было равно Some(5), защитное выражение в первой ветке было бы false, так как остаток от деления 5 на 2 равен 1, который не равен 0. Rust перейдет к второй ветке, которая совпадет, так как во второй ветке нет защитного выражения и, следовательно, она совпадает с любым вариантом Some.

Нет способа выразить условие if x % 2 == 0 внутри шаблона, поэтому защитное выражение в match позволяет нам выразить такую логику. Недостаток этого дополнительного выразительного способа заключается в том, что компилятор не пытается проверить исчерпываемость, когда используются защитные выражения в match.

В листинге 18 - 11 мы упоминали, что можно использовать защитные выражения в match для решения проблемы переопределения переменных (pattern - shadowing). Напомним, что мы создали новую переменную внутри шаблона в выражении match вместо использования переменной вне match. Эта новая переменная означает, что мы не можем проверить значение внешней переменной. Листинг 18 - 27 показывает, как можно использовать защитное выражение в match для решения этой проблемы.

Имя файла: 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);
}

Листинг 18 - 27: Использование защитного выражения в match для проверки равенства с внешней переменной

Этот код теперь напечатает Default case, x = Some(5). Шаблон во второй ветке match не вводит новую переменную y, которая переопределила бы внешнюю y, что означает, что мы можем использовать внешнюю y в защитном выражении. Вместо того чтобы указать шаблон как Some(y), который переопределил бы внешнюю y, мы указываем Some(n). Это создает новую переменную n, которая не переопределяет ничего, так как вне match нет переменной n.

Защитное выражение if n == y не является шаблоном и, следовательно, не вводит новые переменные. Эта y является внешней y, а не новой переопределенной y, и мы можем искать значение, равное внешней y, сравнивая n с y.

Вы также можете использовать оператор или | в защитном выражении в match, чтобы указать несколько шаблонов; условие защитного выражения будет применяться ко всем шаблонам. Листинг 18 - 28 показывает приоритет, когда комбинируется шаблон, использующий |, с защитным выражением в match. Важная часть этого примера заключается в том, что защитное выражение if y применяется к 4, 5 и 6, даже если может показаться, что if y применяется только к 6.

Имя файла: src/main.rs

let x = 4;
let y = false;

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

Листинг 18 - 28: Комбинирование нескольких шаблонов с защитным выражением в match

Условие в match гласит, что ветка совпадает только если значение x равно 4, 5 или 6 и если y равно true. Когда этот код выполняется, шаблон первой ветки совпадает, так как x равно 4, но защитное выражение if y равно false, поэтому первая ветка не выбирается. Код переходит ко второй ветке, которая совпадает, и программа печатает no. Причина в том, что условие if применяется ко всему шаблону 4 | 5 | 6, а не только к последнему значению 6. Другими словами, приоритет защитного выражения в match по отношению к шаблону выглядит так:

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

а не так:

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

После выполнения кода поведение приоритета становится очевидным: если бы защитное выражение применялось только к последнему значению в списке значений, указанных с использованием оператора |, ветка бы совпала и программа напечатала бы yes.

@ Привязки

Оператор at (@) позволяет нам создать переменную, которая хранит значение в то же время, когда мы проверяем это значение на соответствие шаблону. В листинге 18 - 29 мы хотим проверить, что поле id в варианте Message::Hello находится в диапазоне 3..=7. Мы также хотим связать это значение с переменной id_variable, чтобы использовать ее в коде, связанном с этой веткой. Мы могли бы назвать эту переменную id, так же как и поле, но в этом примере мы используем другое имя.

Имя файла: 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}"),
}

Листинг 18 - 29: Использование @ для связывания значения в шаблоне при одновременной проверке

В этом примере будет напечатано Found an id in range: 5. Указав id_variable @ перед диапазоном 3..=7, мы захватываем любое значение, которое соответствует диапазону, и одновременно проверяем, что значение соответствует шаблону диапазона.

Во второй ветке, где в шаблоне указан только диапазон, код, связанный с этой веткой, не имеет переменной, содержащей фактическое значение поля id. Значение поля id могло быть 10, 11 или 12, но код, связанный с этим шаблоном, не знает, какое именно. Код шаблона не может использовать значение поля id, так как мы не сохранили значение id в переменной.

В последней ветке, где мы указали переменную без диапазона, у нас есть доступ к значению в коде этой ветки в переменной с именем id. Причина в том, что мы использовали сокращенную запись для полей структуры. Но в этой ветке мы не применили никаких проверок к значению поля id, как это было в первых двух ветках: любое значение будет соответствовать этому шаблону.

Использование @ позволяет нам проверить значение и сохранить его в переменной в рамках одного шаблона.

Итоги

Поздравляем! Вы завершили лабораторную работу по синтаксису шаблонов. Вы можете попрактиковаться в других лабораторных работах в LabEx, чтобы улучшить свои навыки.