Усі місця, де можна використовувати patterns
Patterns з’являються в низці місць у Rust, і ви використовували їх багато разів, навіть не усвідомлюючи цього! У цьому розділі обговорюються всі місця, де patterns є допустимими.
Арми match
Як обговорювалося в Chapter 6, ми використовуємо patterns в армах виразів
match. Формально, вирази match визначаються як ключове слово match,
значення для зіставлення і одна або більше match-арм, що складаються з
pattern і виразу, який потрібно виконати, якщо значення відповідає pattern цієї
арми, ось так:
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
Наприклад, ось вираз match із Listing 6-5, який зіставляє значення
Option<i32> у змінній x:
match x {
None => None,
Some(i) => Some(i + 1),
}
Patterns у цьому виразі match — це None і Some(i) ліворуч від кожної
стрілки.
Однією з вимог до виразів match є те, що вони мають бути вичерпними в тому
сенсі, що всі можливості для значення у виразі match мають бути враховані.
Один зі способів переконатися, що ви охопили кожну можливість, — мати
універсальний pattern для останньої арми: наприклад, ім’я змінної, яке
збігається з будь-яким значенням, ніколи не може зазнати невдачі і таким чином
охоплює кожен решта випадок.
Конкретний pattern _ зіставлятиметься з будь-чим, але він ніколи не
прив’язується до змінної, тож його часто використовують в останній армі match.
Pattern _ може бути корисним, коли ви хочете, наприклад, ігнорувати будь-яке
значення, що не вказане. Ми детальніше розглянемо pattern _ у розділі
“Ignoring Values in a
Pattern” пізніше в цьому розділі.
Оператори let
До цього розділу ми явно обговорювали використання patterns лише з match та
if let, але насправді ми використовували patterns і в інших місцях, зокрема
в операторах let. Наприклад, розгляньте це просте присвоєння змінної за
допомогою let:
#![allow(unused)]
fn main() {
let x = 5;
}
Щоразу, коли ви використовували такий оператор let, ви використовували
patterns, хоча, можливо, не усвідомлювали цього! Формальніше, оператор let
виглядає так:
let PATTERN = EXPRESSION;
В операторах на кшталт let x = 5;, де в слоті PATTERN стоїть ім’я змінної,
ім’я змінної є просто особливо простою формою pattern. Rust порівнює вираз із
pattern і присвоює всі імена, які знаходить. Отже, у прикладі let x = 5;
x — це pattern, що означає «прив’язати те, що тут відповідає, до змінної
x». Оскільки ім’я x є всім pattern, цей pattern фактично означає
«прив’язати все до змінної x, яке б значення це не було».
Щоб чіткіше побачити аспект зіставлення зі зразком у let, розгляньте Listing
19-1, який використовує pattern з let, щоб деструктурувати кортеж.
fn main() {
let (x, y, z) = (1, 2, 3);
}
Тут ми зіставляємо кортеж із pattern. Rust порівнює значення (1, 2, 3) із
pattern (x, y, z) і бачить, що значення відповідає pattern — тобто бачить,
що кількість елементів у обох однакова — тож Rust прив’язує 1 до x, 2
до y, а 3 до z. Ви можете уявляти цей pattern кортежу як такий, що
вкладено містить три окремі patterns змінних.
Якщо кількість елементів у pattern не збігається з кількістю елементів у кортежі, загальний тип не збіжиться, і ми отримаємо помилку компілятора. Наприклад, Listing 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
Щоб виправити помилку, ми могли б ігнорувати одне або більше значень у
кортежі, використовуючи _ або .., як ви побачите в розділі
“Ignoring Values in a
Pattern”. Якщо проблема полягає
в тому, що в pattern забагато змінних, розв’язанням буде зробити типи
відповідними, видаливши змінні так, щоб кількість змінних дорівнювала кількості
елементів у кортежі.
Умовні вирази if let
У Chapter 6 ми обговорювали, як використовувати вирази if let головним чином
як коротший спосіб записати еквівалент match, який зіставляє лише один
випадок. За бажанням if let може мати відповідний else, що містить код,
який слід виконати, якщо pattern у if let не збігається.
Listing 19-3 показує, що також можливо змішувати вирази if let, else if і
else if let. Це дає нам більшу гнучкість, ніж вираз match, у якому ми
можемо виразити лише одне значення для порівняння з patterns. Також Rust не
вимагає, щоб умови в серії армі if let, else if і else if let
пов’язувалися одна з одною.
Код у Listing 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");
}
}
Якщо користувач вказує улюблений колір, цей колір використовується як фон. Якщо улюблений колір не вказано і сьогодні вівторок, колір фону — зелений. Інакше, якщо користувач вказує свій вік як рядок і ми можемо успішно розібрати його як число, колір буде або фіолетовим, або помаранчевим, залежно від значення числа. Якщо жодна з цих умов не виконується, колір фону буде синім.
Ця умовна структура дає нам змогу підтримувати складні вимоги. З тими
жорстко заданими значеннями, які ми маємо тут, цей приклад виведе 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 виконуватися доти, доки pattern продовжує збігатися. У Listing 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}");
}
}
Цей приклад виводить 1, 2, а потім 3. Метод recv бере перше
повідомлення зі сторони отримувача каналу і повертає Ok(value). Коли ми
вперше побачили recv ще в Chapter 16, ми розпаковували помилку напряму або
взаємодіяли з ним як з ітератором, використовуючи цикл for. Проте, як показує
Listing 19-4, ми також можемо використовувати while let, тому що метод
recv повертає Ok кожного разу, коли надходить повідомлення, доки існує
відправник, а потім породжує Err, щойно сторона відправника від’єднується.
Цикли for
У циклі for значення, яке безпосередньо слідує за ключовим словом for, є
pattern. Наприклад, у for x in y x — це pattern. Listing 19-5
демонструє, як використовувати pattern у циклі for, щоб деструктурувати, або
розібрати на частини, кортеж як частину циклу for.
fn main() {
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{value} is at index {index}");
}
}
Код у Listing 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'). Коли це значення зіставляється з
pattern (index, value), index буде 0, а value буде 'a', що виведе
перший рядок результату.
Параметри функції
Параметри функції також можуть бути patterns. Код у Listing 19-6, який
оголошує функцію з назвою foo, що приймає один параметр з назвою x типу
i32, тепер має бути знайомим.
fn foo(x: i32) {
// code goes here
}
fn main() {}
Частина x — це pattern! Як і з let, ми могли б зіставити кортеж у
аргументах функції з pattern. Listing 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) відповідають
pattern &(x, y), тож x — це значення 3, а y — це значення 5.
Ми також можемо використовувати patterns у списках параметрів замикання так само, як і в списках параметрів функцій, оскільки замикання подібні до функцій, як обговорювалося в Chapter 13.
На цьому етапі ви побачили кілька способів використання patterns, але patterns не працюють однаково в кожному місці, де ми можемо їх використовувати. У деяких місцях patterns мають бути irrefutable; в інших обставинах вони можуть бути refutable. Далі ми обговоримо ці два поняття.