Синтакс паттернов

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

Сопоставление с литералами

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

#![allow(unused)]
fn main() {
let x = 1;

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

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

Сопоставление с именованными переменными

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

Файл: src/main.rs

#![allow(unused)]
fn main() {
let x = Some(5);
let y = 10;

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

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

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

Давайте разберём, что происходит при выполнении выражения match. Паттерн в первой ветке match не совпадает с определённым значением x, поэтому код продолжается.

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

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

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

Чтобы создать выражение match, которое сравнивает значения внешних x и y, вместо введения новой переменной, которая затеняет существующую переменную y, нам нужно использовать вместо этого ограничитель шаблона (match guard). Мы поговорим об ограничителях шаблона позже, в разделе «Добавление условий с помощью ограничителей шаблона».

Сопоставление с несколькими паттернами

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

#![allow(unused)]
fn main() {
let x = 1;

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

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

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

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

#![allow(unused)]
fn main() {
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!

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

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

#![allow(unused)]
fn main() {
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.

Деструктуризация для разбора значений

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

Структуры (Structs)

В листинге 19-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);
}

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

Этот код создаёт переменные a и b, которые соответствуют значениям полей x и y структуры p. Этот пример показывает, что имена переменных в паттерне не обязаны совпадать с именами полей структуры. Однако обычно имена переменных совпадают с именами полей, чтобы было легче запомнить, какие переменные из каких полей получены. Из-за этого распространённого использования и поскольку запись let Point { x: x, y: y } = p; содержит много дублирования, в Rust есть сокращённая запись для паттернов, сопоставляющихся с полями структур: вам нужно только перечислить имена полей структуры, а создаваемые переменные будут иметь те же имена. Листинг 19-13 ведёт себя так же, как код в листинге 19-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);
}

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

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

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

В листинге 19-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})");
        }
    }
}

Листинг 19-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.

Перечисления (Enums)

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

Файл: src/main.rs

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

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

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

Листинг 19-15: Деструктуризация вариантов перечисления, содержащих разные kinds of values

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

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

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

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

Вложенные структуры и перечисления

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

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}");
        }
        _ => (),
    }
}

Листинг 19-16: Сопоставление с вложенными перечислениями

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

Структуры и кортежи

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

#![allow(unused)]
fn main() {
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}

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

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

Игнорирование значений в паттерне

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

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

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

Файл: src/main.rs

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

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

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

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

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

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

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

#![allow(unused)]
fn main() {
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:?}");
}

Листинг 19-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.

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

#![allow(unused)]
fn main() {
let numbers = (2, 4, 8, 16, 32);

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

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

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

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

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

Файл: src/main.rs

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

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

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

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

#![allow(unused)]
fn main() {
// [Этот код не компилируется!]
let s = Some(String::from("Hello!"));

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

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

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

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

#![allow(unused)]
fn main() {
let s = Some(String::from("Hello!"));

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

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

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

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

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

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

#![allow(unused)]
fn main() {
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}"),
}
}

Листинг 19-23: Игнорирование всех полей Point, кроме x, с помощью ..

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

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

Файл: src/main.rs

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

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

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

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

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

Файл: src/main.rs

// [Этот код не компилируется!]
fn main() {
    let numbers = (2, 4, 8, 16, 32);

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

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

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

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
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

error: could not compile `patterns` (bin "patterns") due to 1 previous error

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

Добавление условий с помощью ограничителей шаблона (Match Guards)

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

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

#![allow(unused)]
fn main() {
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 => (),
}
}

Листинг 19-26: Добавление ограничителя шаблона к паттерну

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

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

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

Обсуждая листинг 19-11, мы упомянули, что можем использовать ограничители шаблона для решения нашей проблемы затенения паттерна. Напомним, что мы создали новую переменную внутри паттерна в выражении match вместо использования переменной вне match. Эта новая переменная означала, что мы не могли проверить значение внешней переменной. Листинг 19-27 показывает, как мы можем использовать ограничитель шаблона, чтобы исправить эту проблему.

Файл: 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 = {x:?}, y = {y}");
}

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

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

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

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

#![allow(unused)]
fn main() {
let x = 4;
let y = false;

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

Листинг 19-28: Комбинирование нескольких паттернов с ограничителем шаблона

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

#![allow(unused)]
fn main() {
(4 | 5 | 6) if y => ...
}

а не так:

#![allow(unused)]
fn main() {
4 | 5 | (6 if y) => ...
}

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

Использование привязок @

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

#![allow(unused)]
fn main() {
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!("Found some other id: {id}"),
}
}

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

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

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

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

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

Итог

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

Далее, в предпоследней главе книги, мы рассмотрим некоторые продвинутые аспекты различных возможностей Rust.