Программирование PC Rust: паттерны и выражение match Sun, October 27 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


Rust: паттерны и выражение match Печать
Добавил(а) microsin   

Паттерны, или шаблоны (patterns) это особый синтаксис в Rust для сопоставления (matching) со структурой типов, как сложных, так и простых. Использование шаблонов в сочетании с выражениями соответствия (match expressions) и другими конструкциями дает вам больше контроля над потоком вычислений в программе (program’s control flow). Шаблон состоит из некоторой комбинации следующего:

• Литералы (literals).
• Деструктурированные массивы, перечисления, структуры или кортежи.
• Переменные.
• Групповые символы (wildcards).
• Заполнители (placeholders).

Некоторые примеры паттернов включают x, (a, 3) и Some(Color::Red). В контекстах, в которых паттерны действительны, эти компоненты описывают форму данных. Ваша программа затем сопоставляет значения с паттернами, чтобы определить, корректная ли форма данных для продолжения выполнения части кода.

Для использования паттерна мы сравниваем его с некоторым значением. Если шаблон совпадает со значением, то мы используем части значения в нашем коде. Вспомните match-выражения в главе 6 [2], которые использовали паттерны, как в примере машины сортировки монет. Если значение подходит под форму паттерна, то мы можем использовать именованные части значения. Если же нет, то код, связанный с паттерном, не запустится.

Эта глава (перевод документации [1]) является справочником по всему, что связано с паттернами. Мы рассмотрим подходящие места для использования паттернов, различие между опровержимыми (refutable) и неопровержимыми (irrefutable) паттернами, и различные виды синтаксиса паттернов, с которыми вы можете встретиться. В конце этой главы вы узнаете, как использовать паттерны для четкого выражения многих концепций.

[Можно использовать все места паттернов]

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

match arms. Как обсуждалось в главе 6 [2], мы используем паттерны в ветках (arms) match-выражений. Формально match-выражения определены как ключевое слово match, значение для совпадения (math value) и одно или большее количество match arms, которые состоят из паттерна (pattern) и выражения (expression) для запуска части кода, если значение совпадет с веткой паттерна, примерно так:

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

В качестве примера ниже показано match-выражение из листинга 6-5, совпадающее на значении Option< i32> в переменной x:

match x {
    None => None,
    Some(i) => Some(i + 1),
}

Паттерны в этих match-выражении None и Some(i) на левой стороне каждой стрелки.

Одно из требований к match-выражениям соответствия состоит в том, что они должны быть исчерпывающими в том смысле, что должны учитываться все возможности для значения в match-выражении. Один из способов гарантии, что вы рассмотрели каждую возможность - иметь все захватывающий паттерн в последней ветке arm: например имя переменной, соответствующей любому значению, никогда не может потерпеть неудачу и, таким образом, охватывает каждый оставшийся случай.

Конкретный паттерн _ будет соответствует всему, но он никогда не привязывается к переменной, так что он часто используется в последней ветви match arm. Паттерн _ может быть полезен, например, когда вы хотите игнорировать любое не указанное значение. Более подробно мы рассмотрим паттерн _ в секции "Игнорирование значений в паттерне" этой главы.

Условные выражения if let. В главе 6 [2] мы обсуждали, как использовать выражения if let главным образом как более короткий способ написать эквивалент match, который совпадает только с одним случаем. Опционально if let можно применять с соответствующим else, где будет представлен код, который запустится когда нет соответствия выражения if let.

Листинг 18-1 показывает, как можно смешанно использовать выражения match if let, else if и else if let. Так можно достичь большей гибкости, чем в match-выражении, где мы можем выразить только одно значение для сравнения с паттернами. Также Rust не требует, чтобы ветки последовательность условий соотносились друг с другом.

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

fn main() {
    let favorite_color: Option< &str> = None;
    let is_tuesday = false;
    let age: Result< u8, _> = "34".parse();
if let Some(color) = favorite_color { println!("Using your favorite color, {color}, as the background"); } else if is_tuesday { println!("Tuesday is green day!"); } else if let Ok(age) = age { if age > 30 { println!("Using purple as the background color"); } else { println!("Using orange as the background color"); } } else { println!("Using blue as the background color"); } }

Листинг 18-1. Смешанное использование if let, else if, else if let и else (файл src/main.rs).

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

Эта структура условий позволяет нам поддерживать сложные требования. С жестко закодированными значениями, как это сделано здесь, этот пример будет печатать "Using purple as the background color".

Вы можете видеть, что if let также может вводить затененные переменные (shadowed variables) таким же способом, как могут это делать ветви match arms: строка if let Ok(age) = age вводит новую теневую переменную age, которая содержит значение внутри варианта Ok. Это означает, что нам нужно поместить условие if age > 30 в этот блок: мы не можем комбинировать эти два условия в if let Ok(age) = age && age > 30. Затененная переменная age, которую мы хотим сравнить с 30, недействительна, пока новая область не начнется с фигурной скобки.

Недостаток использования выражений if let состоит в том, что компилятор не проверяет полноту, в то время как это он делает с match-выражениями. Если мы опустили последний блок else, и тем самым пропустили пропустили обработку некоторых случаев, то компилятор не предупредит нас, что возможны некие ошибки логики.

Условные циклы while let. Подобно конструкции if let, условный цикл while let позволяет прокручивать цикл, пока паттерн сохраняет соответствие. В листинге 18-2 мы закодировали цикл while let, использующий вектор в качестве стека, и печатающий значения вектора в обратном порядке от порядка, в котором проталкивались значения.

    let mut stack = Vec::new();
    stack.push(1);
    stack.push(2);
    stack.push(3);
    while let Some(top) = stack.pop() {
        println!("{top}");
    }

Листинг 18-2. Использование цикла while let для печати значений, пока stack.pop() возвращает Some.

Этот пример кода напечатает 3, 2, и затем 1. Метод pop извлечет последний элемент вектора и возвратит Some(value). Если вектор пуст, то pop возвратит None. Цикл while продолжит запускать свое тело в своем блоке, пока pop возвращает Some. Когда pop возвратит None, итерации цикла прекращаются. Мы можем использовать while let для извлечения вызовом pop каждого элемента из нашего стека в векторе.

Циклы for. В цикле for значение, которое непосредственно следует за ключевым словом for, является паттерном. Например, в for x in y переменная x является паттерном. Листинг 18-3 демонстрирует, как использовать паттерн в цикле for для деструктурирования, или разбиения на части кортежа как части цикла for.

    let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() { println!("{value} is at index {index}"); }

Листинг 18-3. Использование паттерна в цикле for для деструктурирования кортежа.

Код в листинге 18-3 напечатает следующее:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2

Мы адаптировали итератор с использованием метода enumerate, так это генерирует значения index и value, извлеченные из кортежа. Первое сгенерированное значение (0, 'a'). Когда это значение соответствует паттерну (index, value), index будет 0, и value будет 'a', печатая первую строку в вывод.

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

let x = 5;

Каждый раз, когда вы использовали такой оператор let, вы использовали шаблоны, хотя, возможно, вы этого не поняли! Более формально инструкция let выглядит так:

let PATTERN = EXPRESSION;

В операторах наподобие let x = 5; с именем переменной в слоте PATTERN, имя переменной просто частная простая форма паттерна. Rust сравнивает выражение expression с паттерном, и присваивает любые найденные имена. Так в примере let x = 5; буква x это паттерн, который означает "привязать то, что соответствует здесь переменной x". Поскольку имя x это весь паттерн, то этот паттерн фактически означает "привязать все к переменной x, независимо от значения".

Чтобы более четко увидеть аспект сопоставления паттерна, рассмотрим листинг 18-4, который использует паттерн вместе с let для деструктурирования кортежа.

    let (x, y, z) = (1, 2, 3);

Листинг 18-4. Использование паттерна для деструктурирования кортежа и создания трех переменных сразу.

Здесь мы установили соответствие кортежа паттерну. Rust сравнивает значение (1, 2, 3) с паттерном (x, y, z) и видит, что значение соответствует паттерну, так что Rust привязывает 1 к x, 2 к y, и 3 к z. Вы можете думать об этом паттерне кортежа как о вложении в него трех отдельных паттернов переменных.

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

    let (x, y) = (1, 2, 3);

Листинг 18-5. Некорректно сконструированный паттерн, в котором переменные не совпадают с количеством элементов в кортеже.

Попытка скомпилировать этот код приведет к ошибке следующего типа:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (x, y) = (1, 2, 3);
  |         ^^^^^^   --------- this expression has type `({integer}, {integer}, {integer})`
  |         |
  |         expected a tuple with 3 elements, found one with 2 elements
  |
  = note: expected tuple `({integer}, {integer}, {integer})`
             found tuple `(_, _)`
For more information about this error, try `rustc --explain E0308`. error: could not compile `patterns` (bin "patterns") due to 1 previous error

Чтобы исправить эту ошибку, мы могли бы проигнорировать один или несколько значений в кортеже использованием _ или .., как вы увидите далее в секции "Игнорирование значений в паттерне". Если у нас слишком много переменных в паттерне, то решение состоит в том, чтобы привести типы в соответствие, удалив переменные, чтобы их количество равнялось количеству элементов в кортеже.

Параметры функции. Параметры функции также могут быть паттернами. Код в листинге 18-6, декларирующий функцию foo, которая принимает параметр с именем x типа i32, должен теперь выглядеть знакомо.

fn foo(x: i32) {
    // здесь должен быть код
}

Листинг 18-6. Сигнатура функции использует паттерны в параметрах.

Часть x это паттерн! Как и в случае с let, мы можем сопоставить кортеж в аргументах функции с паттерном. Листинг 18-7 разделяет значения в кортеже, поскольку мы передаем его в функцию.

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({x}, {y})");
}

fn main() { let point = (3, 5); print_coordinates(&point); }

Листинг 18-7. Функция с параметрами, которая деструктурирует кортеж (файл src/main.rs).

Этот код напечатает "Current location: (3, 5)". Значения &(3, 5) подходят к паттерну &(x, y), так что x получит значение 3, а y значение 5.

Мы можем также использовать паттерны в списках параметров замыкания таким же образом, как и в списках параметров функции, поскольку замыкания подобны функциям, как обсуждалось в главе 13 [3].

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

[Опровержимость: может ли паттерн не соответствовать]

Паттерны бывают в двух формах: опровержимые (refutable) и неопровержимые (irrefutable). Паттерны, которые будут соответствовать любому возможному переданному значению, являются неопровержимыми. Примером может быть x в операторе let x = 5;, потому что x соответствует чему угодно и поэтому не может не совпадать. Паттерны, которые могут не совпадать для некоторого возможного значения, являются опровержимыми. Примером может быть Some(x) в выражении if let Some(x) = a_value, потому что если значение в переменной a_value будет None вместо Some, то паттерн Some(x) не будет соответствовать.

Параметры функции, операторы let и циклы могут принимать только неопровержимые паттерны, потому что программа ничего не может делать, когда значения не соответствуют. Выражения if let и while let принимают опровержимые и неопровержимые паттерны, однако компилятор предупреждает об неопровержимых паттернах, потому что по определению они предназначены для обработки возможного отказа: функциональность условия в его способности выполнять код по-разному в зависимости от успеха или неудачи.

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

Давайте рассмотрим пример того, что происходит, когда мы пытаемся использовать опровержимый паттерн там, где Rust требует неопровержимый паттерн, и наоборот. Листинг 18-8 показывает оператор let, однако для паттерна мы указали Some(x), опровержимый паттерн. Как вы можете ожидать, этот код не скомпилируется.

    let Some(x) = some_option_value;

Листинг 18-8. Попытка использовать опровержимый паттерн вместе с let.

Если some_option_value будет значением None, то не будет соответствие с паттерном Some(x), это означает, что паттерн опровержимый. Однако оператор let может принимать только неопровержимый паттерн, потому что здесь не будет допустимого кода со значением None. Во время компиляции Rust пожалуется, что мы пытаемся использовать опровержимый шаблон там, где требуется неопровержимый шаблон:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding
 --> src/main.rs:3:9
  |
3 |     let Some(x) = some_option_value;
  |         ^^^^^^^ pattern `None` not covered
  |
  = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum`
           with only one variant
  = note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html
  = note: the matched value is of type `Option< i32>`
help: you might want to use `let else` to handle the variant that isn't matched
  |
3 |     let Some(x) = some_option_value else { todo!() };
  |                                     ++++++++++++++++
For more information about this error, try `rustc --explain E0005`. error: could not compile `patterns` (bin "patterns") due to 1 previous error

Поскольку мы не покрыли (и не смогли покрыть!) каждое допустимое значение шаблоном Some (x), Rust правомерно генерирует ошибку компилятора.

Если у нас опровержимый паттерн там, где нужен неопровержимый паттерн, то это можно исправить изменением кода, который использует паттерн: вместо let мы можем применить if let. Тогда если паттерн не соответствует, то код просто пропустит блок в фигурных скобках, давая тем самым допустимый путь продолжить выполнение. Листинг 18-9 показывает, как исправить код в листинге 18-8.

    if let Some(x) = some_option_value {
        println!("{x}");
    }

Листинг 18-9. Использование if let и блока вместе с опровержимым паттерном вместо let.

Теперь этот код исключительно корректен. Однако, если мы вместе с if let применим неопровержимый паттерн (т. е. паттерн, который соответствует всегда), такой как x, как показано в листинге 18-10, то компилятор сгенерирует предупреждение (warning).

    if let x = 5 {
        println!("{x}");
    };

Листинг 18-10. Попытка использовать неопровержимый шаблон вместе с if let.

На этот код Rust пожалуется, что не имеет смысла использовать if let вместе с неопровержимым паттерном:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `if let` pattern
 --> src/main.rs:2:8
  |
2 |     if let x = 5 {
  |        ^^^^^^^^^
  |
  = note: this pattern will always match, so the `if let` is useless
  = help: consider replacing the `if let` with a `let`
  = note: `#[warn(irrefutable_let_patterns)]` on by default
warning: `patterns` (bin "patterns") generated 1 warning Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s Running `target/debug/patterns` 5

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

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

[Синтаксис паттерна]

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

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

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

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

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

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

Листинг 18-11. Выражение match с веткой, которая вводит затененную переменную y (файл src/main.rs).

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

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

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

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

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

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

    let x = 1;
match x { 1 | 2 => println!("один или два"), 3 => println!("три"), _ => println!("любое значение"), }

Этот код напечатает "один или два".

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

    let x = 5;
match x { 1..=5 => println!("от одного до пяти"), _ => println!("какое-то другое значение"), }

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

Компилятор во время компиляции проверяет, что диапазон не пуст, и поскольку единственными типами, для которых 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 показывает структуру Point с двумя полями x и y, которую мы разбиваем на части паттерном с оператором let.

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. Деструктурирование полей структуры на две отдельные переменные (файл src/main.rs).

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

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. Деструктурирование полей структуры с использованием сокращения (файл src/main.rs).

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

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

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

fn main() {
    let p = Point { x: 0, y: 7 };
match p { Point { x, y: 0 } => println!("Точка на оси x с координатой {x}"), Point { x: 0, y } => println!("Точка на оси y с координатой {y}"), Point { x, y } => { println!("Точка не лежит ни на одной из осей, её координаты: ({x}, {y})"); } } }

Листинг 18-14. Деструктурирование и соответствие литеральных значений одной из ветвей выражения match с помощью паттернов (файл src/main.rs).

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

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

В этом конкретном примере значение p соответствует второй ветки match, так как x содержит 0, так что этот код напечатает "Точка на оси y с координатой 7".

Вспомним, что выражение match останавливает проверку сразу, как только оно нашло совпадение с первым по порядку шаблоном, так что даже если мы будем использовать Point { x: 0, y: 0} на осях x и y, то этот код все равно напечатает "Точка на оси x с координатой 0".

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

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 the color to red {r}, green {g}, and blue {b}") } } }

Листинг 18-15. Деструктурирование вариантов перечисления, которые хранят разные виды значений (файл src/main.rs).

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

Для вариантов перечисления без каких-либо данных, наподобие 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 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. Паттерн во второй ветки match также соответствует варианту перечисления Message::ChangeColor, однако вместо этого происходит соответствие внутреннему перечислению Color::Hsv. Мы можем указать эти сложные условия в одном выражении match, хотя используются два перечисления.

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

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

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

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

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

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

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

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

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

Листинг 18-17. Использование _ в сигнатуре функции (файл src/main.rs).

Этот код полностью игнорирует значение 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!("Не могу перезаписать существующее настроенное значение"); } _ => { setting_value = new_setting_value; } }
println!("setting is {setting_value:?}");

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

Этот код напечатает "Не могу перезаписать существующее настроенное значение", и затем установит Some(5). В первой ветке match нам не нужно сопоставлять или использовать значения внутри любого варианта Some, однако мы должны проверить случай, когда setting_value и new_setting_value являются вариантом Some. В таких случаях мы печатаем причину, по которой не поменяли значение setting_value, и оно не меняется.

Во всех других случаях (если либо setting_value, либо new_setting_value это None), что выражено паттерном _ второй ветки, мы хотим позволить присвоить setting_value значение new_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 мы создали две не используемые переменные, но когда этот код компилируется, будет выдано предупреждение только об одной из них.

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

Листинг 18-20. Если имя не используемой переменной начинается на _, по предупреждение по ней не выдается (файл src/main.rs).

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

Обратите внимание, что здесь есть тонкое отличие между использованием только _, и использованием имени, начинающегося на _. Синтаксис _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 у нас структура 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. Игнорирование с помощью ".." всех полей структуры, кроме x.

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

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

fn main() {
    let numbers = (2, 4, 8, 16, 32);
match numbers { (first, .., last) => { println!("Some numbers: {first}, {last}"); } } }

Листинг 18-24. Соответствие только первому и последнему значениям в кортеже, и игнорирование всех других значений (файл src/main.rs).

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

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

fn main() {
    let numbers = (2, 4, 8, 16, 32);
match numbers { (.., second, ..) => { println!("Some numbers: {second}") }, } }

Листинг 18-25. Попытка использовать .. двусмысленно (файл src/main.rs, этот код не скомпилируется).

Когда мы скомпилируем этот пример, то получим следующую ошибку:

$ 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 guard. Конструкция match guard это дополнительное условие if, указанное поле паттерна в ветке match, которое также должно соответствовать для выбора этой ветки. Match guard полезно для выражения более сложных идей, чем позволяет одиночный паттерн.

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

    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. Добавление match guard к паттерну.

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

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

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

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

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

Листинг 18-27. Использование match guard для проверки эквивалентности с внешней переменной (файл src/main.rs).

Этот код теперь напечатает "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, и if y окажется true. Когда этот код запустится, паттерн первой ветке получит соответствие, потому что x равна 4, однако if y равно false, так что первая ветка не будет выбрана. Код переходит ко второй ветке match, которая получает соответствие, и программа напечатает "no". Причина в том, что условие if применяется ко всему паттерну 4 | 5 | 6, не только к последнему 6. Другими словами, приоритет match guard по отношению к паттерну ведет себя так:

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

.. вместо такого поведения:

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

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

@ Bindings. Оператор @ позволяет нам создать переменную, которая хранит значение одновременно с тестированием этого значения на соответствие паттерну. В листинге 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 в этой ветке, как мы делали в первых двух ветках: этому паттерну будет соответствовать любое значение.

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

[Общие выводы]

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

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

[Ссылки]

1. Rust Patterns and Matching site:doc.rust-lang.org.
2. Rust: перечисления (enum) и совпадение шаблонов (match).
3. Rust: итераторы и замыкания.

 

Добавить комментарий


Защитный код
Обновить

Top of Page