Все места, где можно использовать образцы
Образцы встречаются в Rust в самых разных местах, и вы уже активно использовали их, даже не осознавая этого! В этом разделе рассматриваются все места, где образцы являются допустимыми.
Ветви match
Как обсуждалось в Главе 6, мы используем образцы в ветвях выражений match. Формально выражение 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 должны быть учтены. Один из способов убедиться, что вы учли каждую возможность, — иметь универсальный образец для последней ветви: например, имя переменной, соответствующее любому значению, никогда не может завершиться неудачей и таким образом покрывает все оставшиеся случаи.
Конкретный образец _ соответствует чему угодно, но никогда не связывает с переменной, поэтому он часто используется в последней ветви match. Образец _ может быть полезен, когда вы хотите проигнорировать любое значение, не указанное явно, например. Мы подробнее рассмотрим образец _ в разделе «Игнорирование значений в образце» позже в этой главе.
Условные выражения if let
В Главе 6 мы обсудили, как использовать выражения if let в основном как более короткий способ записать эквивалент match, который сопоставляет только один случай. При необходимости if let может иметь соответствующий else, содержащий код для выполнения, если образец в if let не совпадает.
Листинг 19-1 показывает, что также возможно смешивать выражения if let, else if и else if let. Это даёт нам больше гибкости, чем выражение match, в котором мы можем выразить только одно значение для сравнения с образцами. Кроме того, Rust не требует, чтобы условия в серии ветвей if let, else if, else if let были связаны друг с другом.
Код в Листинге 19-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"); } }
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
Построение похоже на if let: условный цикл while let позволяет циклу while выполняться до тех пор, пока образец продолжает соответствовать. В Листинге 19-2 мы показываем цикл 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-2, мы также можем использовать while let, потому что метод recv возвращает Ok каждый раз, когда прибывает сообщение, пока существует отправитель, а затем выдаёт Err, как только сторона отправителя отключается.
Циклы for
В цикле for значение, которое непосредственно следует за ключевым словом for, является образцом. Например, в for x in y x — это образец. Листинг 19-3 демонстрирует, как использовать образец в цикле for для деструктуризации, или разбора, кортежа как части цикла for.
fn main() { let v = vec!['a', 'b', 'c']; for (index, value) in v.iter().enumerate() { println!("{value} is at index {index}"); } }
for для деструктуризации кортежаКод в Листинге 19-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, чтобы он производил значение и индекс для этого значения, помещённые в кортеж. Первое произведённое значение — это кортеж (0, 'a'). Когда это значение сопоставляется с образцом (index, value), index будет 0, а value будет 'a', печатая первую строку вывода.
Операторы 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-4, который использует образец с let для деструктуризации кортежа.
fn main() { let (x, y, z) = (1, 2, 3); }
Здесь мы сравниваем кортеж с образцом. Rust сравнивает значение (1, 2, 3) с образцом (x, y, z) и видит, что значение совпадает с образцом, в том смысле, что количество элементов одинаково в обоих, поэтому Rust связывает 1 с x, 2 с y и 3 с z. Вы можете думать об этом образце кортежа как о вложении трёх отдельных образцов переменных внутри него.
Если количество элементов в образце не совпадает с количеством элементов в кортеже, общий тип не будет совпадать, и мы получим ошибку компиляции. Например, Листинг 19-5 показывает попытку деструктуризации кортежа с тремя элементами в две переменные, что не сработает.
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
Чтобы исправить ошибку, мы могли бы проигнорировать одно или несколько значений в кортеже, используя _ или .., как вы увидите в разделе «Игнорирование значений в образце». Если проблема в том, что у нас слишком много переменных в образце, решение — сделать типы совпадающими, удалив переменные так, чтобы количество переменных равнялось количеству элементов в кортеже.
Параметры функции
Параметры функции также могут быть образцами. Код в Листинге 19-6, который объявляет функцию с именем foo, принимающую один параметр с именем x типа i32, должен уже выглядеть знакомо.
fn foo(x: i32) { // code goes here } 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.
На этом этапе вы увидели несколько способов использования образцов, но образцы не работают одинаково во всех местах, где мы можем их использовать. В некоторых местах образцы должны быть неопровержимыми; в других обстоятельствах они могут быть опровержимыми. Мы обсудим эти два понятия далее.