Где можно использовать образцы
Образцы встречаются в Rust во многих местах, и вы уже много ими пользовались, сами того не замечая! В этом разделе перечислены все места, где образцы допустимы.
Ветки match
Как говорилось в главе 6, образцы используются в ветках выражений match.
Формально выражение match состоит из ключевого слова match, значения для
сопоставления и одной или нескольких веток. Каждая ветка — это образец и
выражение, которое выполняется, если значение совпало с образцом ветки:
match ЗНАЧЕНИЕ {
ОБРАЗЕЦ => ВЫРАЖЕНИЕ,
ОБРАЗЕЦ => ВЫРАЖЕНИЕ,
ОБРАЗЕЦ => ВЫРАЖЕНИЕ,
}
Например, вот выражение match из листинга 6-5, которое сопоставляет значение
Option<i32> в переменной x:
match x {
None => None,
Some(i) => Some(i + 1),
}
Образцы в этом выражении match — None и Some(i) слева от каждой стрелки.
Одно из требований к выражениям match — они должны быть исчерпывающими: все
возможные значения в выражении match должны быть учтены. Один из способов
гарантировать, что вы покрыли все случаи, — сделать последнюю ветку
«для всего остального». Например, образец из одного имени переменной
совпадает с любым значением, поэтому такая ветка покрывает все оставшиеся
случаи.
Особый образец _ совпадает с чем угодно, но никогда не привязывается к
переменной, поэтому его часто используют в последней ветке. Образец _ удобен,
например, когда нужно проигнорировать все значения, не указанные явно. Подробнее
об образце _ — в разделе «Игнорирование значений в
образце» дальше в этой главе.
Инструкции let
До этой главы мы явно говорили об образцах только в связи с match и if let,
но на самом деле пользовались ими и в других местах — в том числе в
инструкциях let. Возьмём, например, простое присваивание с let:
#![allow(unused)]
fn main() {
let x = 5;
}
Каждый раз, когда вы писали подобную инструкцию let, вы пользовались
образцами, даже если не догадывались об этом! Формально инструкция let
выглядит так:
let ОБРАЗЕЦ = ВЫРАЖЕНИЕ;
В инструкциях вроде let x = 5;, где на месте ОБРАЗЦА стоит имя переменной,
имя переменной — просто очень простая разновидность образца. Rust сравнивает
выражение с образцом и привязывает все найденные имена. Так что в примере
let x = 5; x — это образец, который означает «привязать то, что здесь
совпадёт, к переменной x». Раз имя x — это весь образец целиком, он, по
сути, означает «привязать всё к переменной x, каким бы ни было значение».
Чтобы яснее увидеть, что в let происходит сопоставление с образцом,
посмотрите на листинг 19-1, где образец в let разбирает кортеж.
fn main() {
let (x, y, z) = (1, 2, 3);
}
Здесь кортеж сопоставляется с образцом. Rust сравнивает значение (1, 2, 3) с
образцом (x, y, z) и видит, что значение совпадает с образцом — количество
элементов в них одинаковое, — поэтому привязывает 1 к x, 2 к y и 3 к
z. Этот образец кортежа можно считать тремя вложенными в него образцами
отдельных переменных.
Если количество элементов в образце не совпадает с количеством элементов кортежа, типы целиком не совпадут, и будет ошибка компиляции. Например, в листинге 19-2 попытка разобрать кортеж из трёх элементов на две переменные — так не получится.
fn main() {
let (x, y) = (1, 2, 3);
}
Попытка скомпилировать этот код даёт ошибку типов:
$ 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
Чтобы исправить ошибку, можно проигнорировать одно или несколько значений
кортежа с помощью _ или .., как показано в разделе «Игнорирование значений
в образце». А если проблема в том,
что переменных в образце слишком много, нужно убрать лишние, чтобы их число
совпало с числом элементов кортежа и типы совпали.
Условные выражения if let
В главе 6 мы обсуждали выражения if let в основном как краткую запись
match, который обрабатывает только один случай. При желании у if let может
быть else с кодом, который выполнится, если образец в if let не совпал.
В листинге 19-3 видно, что можно также сочетать выражения if let, else if и
else if let. Это даёт больше гибкости, чем выражение match, где с образцами
сравнивается лишь одно значение. К тому же Rust не требует, чтобы условия в
цепочке веток if let, else if и else if let были связаны друг с другом.
Код в листинге 19-3 выбирает цвет фона, проверяя несколько условий подряд. Для этого примера мы создали переменные с вписанными в код значениями — в настоящей программе они могли бы приходить из пользовательского ввода.
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");
}
}
if let, else if, else if let и elseЕсли пользователь указал любимый цвет, фоном становится он. Если любимый цвет не указан, а сегодня вторник, фон зелёный. Иначе, если пользователь указал возраст строкой и её удалось разобрать как число, цвет будет фиолетовым или оранжевым в зависимости от числа. Если ни одно из этих условий не выполнено, фон синий.
Такая условная структура позволяет поддерживать сложные требования. С
заданными в коде значениями этот пример выведет
Using purple as the background color.
Видно, что if let, как и ветки match, может вводить новые переменные,
затеняющие существующие: строка if let Ok(age) = age вводит новую переменную
age со значением изнутри варианта Ok, затеняя прежнюю age. Поэтому условие
if age > 30 нужно поместить внутрь этого блока: объединить два условия в
if let Ok(age) = age && age > 30 нельзя. Новая age, которую мы хотим
сравнить с 30, становится действительной только с началом новой области
видимости — с фигурной скобки.
Недостаток выражений if let в том, что компилятор не проверяет, учтены ли все
случаи, а с выражениями match проверяет. Если бы мы убрали последний блок
else и тем самым пропустили какие-то случаи, компилятор не предупредил бы нас
о возможной логической ошибке.
Условные циклы while let
Условный цикл while let, устроенный похоже на if let, позволяет циклу
while выполняться, пока образец продолжает совпадать. В листинге 19-4 цикл
while let ждёт сообщений, пересылаемых между потоками, — только здесь
проверяется Result, а не Option.
fn main() {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
for val in [1, 2, 3] {
tx.send(val).unwrap();
}
});
while let Ok(value) = rx.recv() {
println!("{value}");
}
}
while let выводит значения, пока rx.recv() возвращает OkЭтот пример выведет 1, 2, а затем 3. Метод recv забирает первое сообщение
с принимающей стороны канала и возвращает Ok(value). Когда мы впервые увидели
recv в главе 16, мы сразу разворачивали ошибку или работали с приёмником как
с итератором в цикле for. Но, как показывает листинг 19-4, можно использовать и
while let: метод recv возвращает Ok каждый раз, когда приходит сообщение,
пока существует отправитель, а когда отправляющая сторона отключается — Err.
Циклы for
В цикле for значение сразу после ключевого слова for — это образец.
Например, в for x in y образец — x. В листинге 19-5 показано, как с помощью
образца в цикле for разобрать на части (деструктурировать) кортеж.
fn main() {
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{value} is at index {index}");
}
}
for разбирает кортежКод из листинга 19-5 выведет:
$ 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, чтобы он выдавал значение вместе с
его индексом в виде кортежа. Первое выданное значение — кортеж (0, 'a').
Когда оно сопоставляется с образцом (index, value), index будет равен 0, а
value — 'a', и выведется первая строка вывода.
Параметры функций
Параметры функций тоже могут быть образцами. Код в листинге 19-6, который
объявляет функцию foo с одним параметром x типа i32, должен быть вам уже
знаком.
fn foo(x: i32) {
// здесь код
}
fn main() {}
Часть x — это образец! Как и с let, кортеж в аргументах функции можно
сопоставить с образцом. В листинге 19-7 значения кортежа разделяются прямо при
передаче его в функцию.
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({x}, {y})");
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
Этот код выводит Current location: (3, 5). Значения &(3, 5) совпадают с
образцом &(x, y), так что x — это значение 3, а y — 5.
Образцы можно использовать и в списках параметров замыканий — так же, как в списках параметров функций, ведь замыкания похожи на функции, как мы говорили в главе 13.
Итак, вы увидели несколько способов использовать образцы, но работают образцы не везде одинаково. В одних местах образцы обязаны быть неопровержимыми, в других могут быть опровержимыми. Об этих двух понятиях и пойдёт речь дальше.