Rust - Basic - 17 - Сопоставление с образцом

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

Кратко

немного синтаксического сахара, довольно разнородно; советую сразу смотреть Origin

Источник

https://doc.rust-lang.org/book/ch18-00-patterns.html

…

Синтаксис образцов

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

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

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


    let x = 1;

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

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

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

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

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


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

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

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

Образец во второй ветке вводит новую переменную 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, а не вводит затенённую переменную, нам понадобился бы match guard. О match guards мы поговорим позже, в разделе «Дополнительные условия с match guards» .

Несколько образцов

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


    let x = 1;

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

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

Сопоставление диапазонов значений через ..=

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


    let x = 5;

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

Если x равен 1, 2, 3, 4 или 5, совпадёт первая ветка. Этот синтаксис удобнее, чем выражать ту же мысль оператором |: вместо 1..=5 пришлось бы писать 1 | 2 | 3 | 4 | 5.

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

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


    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.

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

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

Деструктуризация структур

Листинг 18-12 показывает struct 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; содержит много дублирования, для образцов, совпадающих с полями структуры, есть сокращение: достаточно перечислить имена полей структуры, и созданные образцом переменные получат те же имена. Листинг 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.

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

Ранее в книге мы уже деструктурировали перечисления, например когда деструктурировали Option<i32> в листинге 6-5 из главы 6. Одну деталь мы не упомянули явно: образец для деструктуризации перечисления должен соответствовать тому, как определены хранящиеся в перечислении данные. Для примера в листинге 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() {
    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 {} and in the y direction {}",
                x, y
            );
        }
        Message::Write(text) => println!("Text message: {}", text),
        Message::ChangeColor(r, g, b) => println!(
            "Change the color to red {}, green {}, and blue {}",
            r, g, b
        ),
    }
}

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

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

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

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

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

Деструктуризация вложенных структур и перечислений

До сих пор все наши примеры сопоставляли структуры или перечисления глубиной в один уровень. Сопоставление работает и с вложенными элементами!

Например, можно переделать код из листинга 18-15, чтобы поддержать цвета RGB и HSV в сообщении ChangeColor, как показано в листинге 18-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 the color to red {}, green {}, and blue {}",
            r, g, b
        ),
        Message::ChangeColor(Color::Hsv(h, s, v)) => println!(
            "Change the color to hue {}, saturation {}, and value {}",
            h, s, v
        ),
        _ => (),
    }
}

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

Образец первой ветки в выражении match совпадает с вариантом перечисления Message::ChangeColor, содержащим вариант Color::Rgb; затем образец привязывается к трём внутренним значениям i32. Образец второй ветки тоже совпадает с вариантом Message::ChangeColor, но внутреннее перечисление совпадает уже с вариантом Color::Hsv. Такие сложные условия можно задать в одном выражении match, хотя в нём участвуют два перечисления.

Деструктуризация структур и кортежей

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


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

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

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

Игнорирование значений в образце

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

Игнорирование целого значения через _

Мы использовали подчёркивание (_) как образец-шаблон, который совпадает с любым значением, но не привязывается к нему. Хотя образец _ особенно полезен последней веткой в выражении 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 показывает код, управляющий значением настройки. Бизнес-требования таковы: пользователь не должен иметь возможности перезаписать существующую настройку, но может снять настройку и задать значение, если она сейчас не задана.


    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 показывает пример игнорирования второго и четвёртого значений в кортеже из пяти элементов.


    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 всё равно привязывает значение к переменной, а _ не привязывает вообще. Чтобы показать случай, где эта разница важна, листинг 18-21 даст нам ошибку.


    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 не перемещается в _.


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

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

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

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

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

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

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


    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: попытка использовать .. неоднозначным образом

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


$ 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` due to 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 guards полезны для выражения более сложных идей, чем допускает один образец.

Условие может использовать переменные, созданные в образце. Листинг 18-26 показывает match, где первая ветка имеет образец Some(x) и match guard if x < 5.


    let num = Some(4);

    match num {
        Some(x) if x < 5 => println!("less than five: {}", x),
        Some(x) => println!("{}", x),
        None => (),
    }

Листинг 18-26: добавление match guard к образцу

Этот пример выведет less than five: 4. Когда num сравнивается с образцом в первой ветке, он совпадает, потому что Some(4) совпадает с Some(x). Затем match guard проверяет, меньше ли значение в x чем 5, и поскольку это так, выбирается первая ветка.

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

Выразить условие if x < 5 внутри образца невозможно, поэтому match guard даёт нам возможность выразить эту логику.

В листинге 18-11 мы упоминали, что match guards позволяют решить проблему затенения образцом. Вспомните: внутри образца в выражении match создавалась новая переменная вместо использования переменной вне match. Эта новая переменная означала, что мы не могли проверить значение внешней переменной. Листинг 18-27 показывает, как исправить проблему с помощью match guard.

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

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

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

Match guard if n == y — не образец и поэтому не вводит новых переменных. Эта y является внешней y, а не новой затенённой y, и мы можем искать значение, равное внешней y, сравнивая n с y.

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


    let x = 4;
    let y = false;

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

Листинг 18-28: сочетание нескольких образцов с match guard

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


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

а не так:


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

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

[@-привязки

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


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

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

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

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

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

Использование @ позволяет проверить значение и сохранить его в переменную в одном образце.