Синтаксис образцов
В этом разделе собран весь синтаксис, допустимый в образцах, и обсуждается, зачем и когда может пригодиться каждый его вариант.
Сопоставление с литералами
Как вы видели в главе 6, образцы можно сопоставлять прямо с литералами. Вот несколько примеров:
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. Посмотрите на образцы в ветках и на
println! в конце и попробуйте понять, что выведет код, прежде чем запускать
его или читать дальше.
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}");
}
match с веткой, которая вводит новую переменную, затеняющую существующую переменную yРазберём, что происходит при выполнении выражения match. Образец первой ветки
не совпадает со значением x, так что выполнение идёт дальше.
Образец второй ветки вводит новую переменную y, которая совпадёт с любым
значением внутри Some. Мы находимся в новой области видимости внутри
выражения match, поэтому это новая переменная y, а не та y со значением
10, что мы объявили в начале. Эта новая привязка y совпадёт с любым
значением внутри Some, а у нас в x как раз Some. Поэтому новая 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».
Несколько образцов
В выражениях match можно сопоставлять сразу с несколькими образцами с помощью
синтаксиса | — это оператор «или» для образцов. Например, в следующем коде
значение x сопоставляется с ветками, и у первой из них есть вариант
«или»: если значение x совпадёт с любым из значений этой ветки, выполнится
её код.
fn main() {
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}
Этот код выведет one or two.
Диапазоны значений с ..=
Синтаксис ..= позволяет сопоставлять с диапазоном значений, включающим обе
границы. В следующем коде, если образец совпадёт с любым значением из
заданного диапазона, выполнится соответствующая ветка:
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 и числовых значений, поэтому
диапазоны допустимы только для числовых значений и char.
Вот пример с диапазонами значений char:
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.
Деструктуризация: разбираем значения на части
С помощью образцов можно также деструктурировать структуры, перечисления и кортежи, чтобы пользоваться отдельными частями этих значений. Разберём каждый случай.
Структуры
В листинге 19-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);
}
Этот код создаёт переменные a и b, совпадающие со значениями полей x и
y структуры p. Пример показывает, что имена переменных в образце не
обязаны совпадать с именами полей структуры. Но обычно имена переменных берут
такими же, как имена полей, чтобы легче было помнить, какая переменная из
какого поля. Раз так делают часто, а запись let Point { x: x, y: y } = p;
полна повторений, в Rust есть сокращённая запись образцов для полей структуры:
достаточно перечислить имена полей, и созданные образцом переменные получат
те же имена. Листинг 19-13 ведёт себя так же, как листинг 19-12, только
переменные, созданные образцом в let, называются 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);
}
Этот код создаёт переменные x и y, совпадающие с полями x и y
переменной p. В итоге в переменных x и y оказываются значения из
структуры p.
В образце структуры можно указывать и литералы, а не только создавать переменные для всех полей. Так можно проверить, что некоторые поля имеют определённые значения, и одновременно создать переменные для остальных.
В листинге 19-14 выражение match делит значения Point на три случая: точки
прямо на оси x (когда y = 0), на оси y (x = 0) и не на осях.
struct Point {
x: i32,
y: i32,
}
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})");
}
}
}
Первая ветка совпадёт с любой точкой на оси 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.
Перечисления
Мы уже деструктурировали перечисления в этой книге (например, в листинге 6-5
главы 6), но ещё не говорили явно, что образец для деструктуризации
перечисления повторяет то, как определены данные внутри перечисления. Для
примера в листинге 19-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 color to red {r}, green {g}, and blue {b}");
}
}
}
Этот код выведет 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 можно переделать так, чтобы сообщение
ChangeColor поддерживало цвета в RGB и HSV, как в листинге 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}");
}
_ => (),
}
}
Образец первой ветки выражения match совпадает с вариантом
Message::ChangeColor, содержащим вариант Color::Rgb, и привязывает три
внутренних значения i32. Образец второй ветки тоже совпадает с вариантом
Message::ChangeColor, но внутреннее перечисление — Color::Hsv. Такие сложные
условия можно задать в одном выражении match, хотя в них участвуют два
перечисления.
Структуры и кортежи
Образцы для деструктуризации можно сочетать и вкладывать друг в друга ещё более сложными способами. Следующий пример показывает сложную деструктуризацию: структуры и кортежи вложены в кортеж, и из всего этого извлекаются примитивные значения.
fn main() {
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
Так сложные типы можно разбирать на составные части и пользоваться нужными значениями по отдельности.
Деструктуризация с помощью образцов — удобный способ пользоваться частями значений, например значением каждого поля структуры, независимо друг от друга.
Игнорирование значений в образце
Вы уже видели, что иногда полезно игнорировать значения в образце — например,
в последней ветке match, чтобы поймать «всё остальное»: такая ветка ничего не
делает, но учитывает все оставшиеся возможные значения. Игнорировать значения
целиком или по частям можно несколькими способами: образцом _ (его вы уже
видели), образцом _ внутри другого образца, именем, начинающимся с
подчёркивания, или .., чтобы проигнорировать оставшиеся части значения.
Разберём, как и зачем использовать каждый из них.
Значение целиком: _
Мы уже использовали подчёркивание как подстановочный образец, который
совпадает с любым значением, но не привязывает его. Особенно он полезен в
последней ветке выражения match, но использовать его можно в любом образце, в
том числе в параметрах функций, как в листинге 19-17.
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {y}");
}
fn main() {
foo(3, 4);
}
_ в сигнатуре функцииЭтот код полностью проигнорирует значение 3, переданное первым аргументом, и
выведет This code only uses the y parameter: 4.
Обычно, когда какой-то параметр функции больше не нужен, сигнатуру меняют так, чтобы неиспользуемого параметра в ней не было. Но игнорирование параметра бывает особенно полезно, например, когда вы реализуете трейт: сигнатура должна быть определённой, а телу функции в вашей реализации один из параметров не нужен. Так вы избегаете предупреждения компилятора о неиспользуемом параметре, которое появилось бы, если бы вы дали ему имя.
Части значения: вложенный _
_ можно использовать и внутри другого образца, чтобы игнорировать лишь часть
значения, — например, когда нужно проверить только часть значения, а остальные
части в коде не понадобятся. В листинге 19-18 — код, управляющий значением
настройки. По бизнес-требованиям пользователь не может перезаписать уже
заданное значение настройки, но может сбросить настройку и задать ей значение,
если сейчас оно не задано.
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:?}");
}
Some, когда значение внутри Some не нужноЭтот код выведет Can't overwrite an existing customized value, а затем
setting is Some(5). В первой ветке нам не нужно ни сопоставлять, ни
использовать значения внутри вариантов Some, но нужно проверить случай,
когда и setting_value, и new_setting_value — варианты Some. В этом случае
мы выводим, почему setting_value не меняется, и не меняем его.
Во всех остальных случаях (когда setting_value или new_setting_value равно
None) срабатывает образец _ во второй ветке, и мы записываем
new_setting_value в setting_value.
Подчёркивания можно использовать и в нескольких местах одного образца, чтобы игнорировать определённые значения. В листинге 19-19 пример, где игнорируются второе и четвёртое значения кортежа из пяти элементов.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}");
}
}
}
Этот код выведет Some numbers: 2, 8, 32, а значения 4 и 16 будут
проигнорированы.
Неиспользуемая переменная: имя с _
Если создать переменную и нигде её не использовать, Rust обычно выдаёт предупреждение: неиспользуемая переменная может быть признаком бага. Но иногда полезно создать переменную, которую вы пока использовать не будете, — например, при прототипировании или в самом начале проекта. Тогда можно попросить Rust не предупреждать о неиспользуемой переменной, начав её имя с подчёркивания. В листинге 19-20 мы создаём две неиспользуемые переменные, но при компиляции предупреждение будет только об одной из них.
fn main() {
let _x = 5;
let y = 10;
}
Здесь мы получаем предупреждение о том, что не используется переменная y, а о
_x — нет.
Учтите тонкую разницу между одиночным _ и именем, начинающимся с
подчёркивания. Запись _x всё равно привязывает значение к переменной, а _
не привязывает вообще ничего. Чтобы показать, когда эта разница важна, вот
листинг 19-21, который выдаст ошибку.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
Будет ошибка: значение s всё равно переместится в _s, и снова использовать
s будет нельзя. А одиночное подчёркивание никогда ничего не привязывает.
Листинг 19-22 скомпилируется без ошибок, потому что s не перемещается в _.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
}
Этот код прекрасно работает: s ни к чему не привязывается, а значит, и не
перемещается.
Оставшиеся части значения: ..
Если у значения много частей, синтаксисом .. можно взять нужные части и
проигнорировать остальные, не выписывая подчёркивание для каждой
игнорируемой. Образец .. игнорирует все части значения, которые не
сопоставлены явно в остальной части образца. В листинге 19-23 структура
Point хранит координату в трёхмерном пространстве. В выражении match нам
нужна только координата x, а значения полей y и z мы игнорируем.
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}"),
}
}
Point, кроме x, с помощью ..Мы указываем x, а затем просто добавляем образец ... Это быстрее, чем писать
y: _ и z: _, особенно когда у структуры много полей, а важны лишь одно-два.
.. растягивается на столько значений, сколько нужно. В листинге 19-24 показано,
как использовать .. с кортежем.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
Здесь первое и последнее значения сопоставляются с first и last, а ..
совпадает со всем, что посередине, и игнорирует это.
Однако использование .. должно быть однозначным. Если непонятно, какие
значения сопоставлять, а какие игнорировать, Rust выдаст ошибку. В листинге
19-25 — пример неоднозначного использования .., поэтому код не скомпилируется.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
.. неоднозначноПри компиляции этого примера получим ошибку:
$ 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
Охранник (match guard) — дополнительное условие if, указанное после образца
в ветке match. Чтобы ветка была выбрана, должно выполниться и оно. Охранники
полезны, когда нужно выразить идею сложнее, чем позволяет один образец. Учтите,
однако, что они есть только в выражениях match, но не в if let и
while let.
В условии можно использовать переменные, созданные в образце. В листинге 19-26 —
match, у первой ветки которого образец Some(x) и охранник if x % 2 == 0
(истинный, если число чётное).
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 => (),
}
}
Этот пример выведет The number 4 is even. Когда num сравнивается с образцом
первой ветки, он совпадает: Some(4) совпадает с Some(x). Затем охранник
проверяет, равен ли 0 остаток от деления x на 2, — и раз равен, выбирается
первая ветка.
Если бы num был Some(5), охранник первой ветки дал бы false: остаток от
деления 5 на 2 равен 1, а не 0. Тогда Rust перешёл бы ко второй ветке, и она
совпала бы: охранника у неё нет, а значит, она совпадает с любым вариантом
Some.
Выразить условие if x % 2 == 0 внутри образца никак нельзя, а охранник даёт
такую возможность. Обратная сторона этой выразительности — когда в выражении
есть охранники, компилятор не пытается проверить, что учтены все случаи.
Обсуждая листинг 19-11, мы упоминали, что охранники помогут решить нашу
проблему с затенением в образце. Вспомните: внутри образца в выражении match
мы создали новую переменную вместо того, чтобы использовать переменную снаружи
match. Из-за этой новой переменной сравнить значение с внешней переменной
было нельзя. В листинге 19-27 показано, как исправить это с помощью охранника.
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}");
}
Теперь код выведет Default case, x = Some(5). Образец второй ветки не вводит
новую переменную y, которая затенила бы внешнюю y, так что внешнюю y можно
использовать в охраннике. Вместо образца Some(y), который затенил бы внешнюю
y, мы пишем Some(n). Так создаётся новая переменная n, которая ничего не
затеняет: снаружи match переменной n нет.
Охранник if n == y — не образец, поэтому новых переменных не вводит. Эта y —
внешняя y, а не новая, затеняющая её, и, сравнивая n с y, можно искать
значение, равное значению внешней y.
В охраннике можно использовать и оператор «или» |, чтобы указать несколько
образцов; условие охранника будет применяться ко всем ним. Листинг 19-28
показывает приоритет при сочетании образца с | и охранника. Главное в этом
примере то, что охранник if y применяется к 4, 5 и 6, хотя может
показаться, что if y относится только к 6.
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
Условие ветки говорит, что она совпадает, только если значение x равно 4,
5 или 6, и при этом y истинно. При выполнении образец первой ветки
совпадает, потому что x равен 4, но охранник if y даёт false, так что
первая ветка не выбирается. Код переходит ко второй ветке, она совпадает, и
программа выводит no. Дело в том, что условие if относится ко всему образцу
4 | 5 | 6, а не только к последнему значению 6. Иначе говоря, приоритет
охранника относительно образца такой:
(4 | 5 | 6) if y => ...
а не такой:
4 | 5 | (6 if y) => ...
Запустив код, в этом легко убедиться: если бы охранник применялся только к
последнему значению из списка через |, ветка совпала бы, и программа вывела бы
yes.
Привязки с @
Оператор @ (at) позволяет создать переменную, хранящую значение, и
одновременно проверить, совпадает ли это значение с образцом. В листинге 19-29
мы хотим проверить, что поле id у Message::Hello лежит в диапазоне 3..=7.
И ещё хотим привязать значение к переменной id, чтобы пользоваться им в коде
ветки.
fn main() {
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id @ 3..=7 } => {
println!("Found an id in range: {id}")
}
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {id}"),
}
}
@ привязываем значение в образце и одновременно проверяем егоЭтот пример выведет Found an id in range: 5. Указав id @ перед диапазоном
3..=7, мы сохраняем совпавшее с диапазоном значение в переменной id и
одновременно проверяем, что значение совпало с образцом-диапазоном.
Во второй ветке в образце указан только диапазон, и в коде этой ветки нет
переменной с фактическим значением поля id. Значение id могло быть 10, 11 или
12, но код этой ветки не знает, какое именно. Воспользоваться значением
поля id он не может: мы не сохранили его в переменную.
В последней ветке, где указана переменная без диапазона, значение доступно в
коде ветки в переменной id: здесь мы воспользовались сокращённой записью полей
структуры. Но в этой ветке значение поля id никак не проверяется, в отличие от
первых двух: с этим образцом совпадёт любое значение.
@ позволяет в одном образце и проверить значение, и сохранить его в
переменную.
Итоги
Образцы в Rust очень полезны для различения разных видов данных. В выражениях
match Rust гарантирует, что образцы покрывают все возможные значения, — иначе
программа не скомпилируется. Образцы в инструкциях let и параметрах функций
делают эти конструкции полезнее: позволяют разбирать значения на части и
присваивать эти части переменным. Образцы можно делать простыми или сложными —
как того требует задача.
Дальше, в предпоследней главе книги, мы рассмотрим продвинутые стороны самых разных возможностей Rust.