Паттерны представляют шаблоны, с которыми сравнивается некоторое значение. Например, возьмем конструкцию match. Ее формальное определение:
match значение{
паттерн1 => действия1,
паттерн2 => действия2,
паттерн3 => действия3
}
Оператор match принимает некоторое значение и сравнивает его с набором паттернов. Если значение соответствует одному из паттернов, то
выполняются действия, предусмотренные для данного паттерна.
Возьмем простейший пример:
fn main(){
let n = 2;
match n {
1 => println!("один"),
2 => println!("два"),
3 => println!("три"),
_ => println!("непонятно")
}
}
Здесь значение переменной n сравнивается с набором паттернов, в качестве которых выступают обычные числа. В данном случае n = 2, поэтому это
значение соответствует паттерну "2", следовательно будет выполняться выражение println!("два").
Однако паттерны необязательно должны представлять отдельные значения типа чисел или строк. Паттерны в Rust одновременно более гибкий и в то же время более мощный инструмент. Рассмотрим отдельные случаи применения паттернов.
В качестве паттерна может выступать набор значений. Например, с помощью оператора | мы можем указать два альтернативных выражения, которым должно соответствовать значение:
fn main(){
let n = 2;
match n {
1 | 2 => println!("один или два"),
3 => println!("три"),
_ => println!("непонятно")
}
}
Шаблон 1 | 2 указывает, что значение должно соответствовать либо числу 1, либо числу 2.
С помощью оператора последовательности ..= можно задать диапазон значений:
fn main(){
let n = 5;
match n {
1..=4 => println!("от одного до четырех"),
5..=9 => println!("от пяти до девяти"),
_ => println!("непонятно")
}
}
Шаблон 1..=4 указывает, что значение должно находиться в диапазоне от 1 до 4 включительно.
Конструкция match также позволяет сопоставить значения кортежа с некоторым набором шаблонов и выполнить соответствующие действия:
fn main() {
let user = ("Bob", 37);
match user{
("Tom", 36) => println!("name: Tom, age: 36"),
("Bob", 37) => println!("name: Bob, age: 37"),
_ => println!("Undefined")
}
}
Здесь кортеж сопоставлятся с набором шаблонов, которые представляют кортежи. Поскольку в данном случае значения кортежа user соответствует шаблону ("Bob", 37), то будет выполняться выражение println!("name: Bob, age: 37")
При этом при помощи шаблонов можно сравнивать только с частью значений, а также можно получать некоторые значения в переменные. Например:
fn main() {
let user = ("Bob", 37);
match user{
("Tom", 36) => println!("name: Tom, age: 36"),
(name, 37) => println!("name: {}", name),
_ => println!("Undefined")
}
}
Шаблон (name, 37) представляет шаблон, которому соответствует любой кортеж из двух элементов, где второй компонент представляет число 37.
И если кортеж соответствует этому шаблону, его первый компонент помещается в переменную name, которую мы можем использовать в действиях,
предусмотренных для этого шаблона.
Более того мы можем целиком уйти от конкретных значений:
fn main(){
let user = ("Bob", 37);
match user{
("Tom", age) => println!("Pattern1. Name: Tom, age: {}", age),
(name, 22) => println!("Pattern2. Name: {}, age: 22", name),
(name, age) => println!("Pattern3. Name: {}, age: {}", name, age),
}
}
Два первых шаблона в конструкции match используют по одной переменной либо для первого, либо для второго компонента кортежа. Тогда как третий шаблон
использует две переменных - (name, age) и соответствует любому кортежу, который имеет два компонента. Поскольку этот шаблон покрывает все возможные
кортежи с двумя элементами, нам не надо определять шаблон _=> для случаев, которые не вписываются в предыдущие шаблоны.
Аналогично кортежам в конструкциях match можно использовать структуры. Например, мы можем определить для структуры шаблон с конкретными значениями:
struct Person { name: String, age: u8 }
fn main(){
let bob = Person{ name: "Bob".to_string(), age: 41};
let _username = "Bob".to_string();
match bob{
Person{name: _username, age: 41} => println!("Name: Bob, age: 41"),
_ => println!("Undefined person"),
}
}
Здесь объект структуры Person сравнивается с шаблоном Person{name: _username, age: 41}, то есть с некоторой структурой, где поле name
равно переменной _username, а поле age - числу 41.
Обратите внимание, что в шаблонах нельзя использовать вызовы функций, поэтому шаблон выглядит так:
Person{name: _username, age: 41}
А не так:
Person{name: "Bob".to_string(), age: 41}
Также в шаблонах для структур можно определять переменные:
struct Person { name: String, age: u8 }
fn main(){
let tom = Person{ name: "Tom".to_string(), age: 36};
let _username = "Bob".to_string();
match tom{
Person{name: _username, age: 41} => println!("Name: Bob, age: 41"),
Person{name, age: 36} => println!("Name: {}, age: 36", name),
Person{name, age} => println!("Name: {}, age: {}", name, age)
}
}
Здесь первый шаблон, как и в предыдущем примере устанавливает конкретные значения.
Второй шаблон - Person{name, age: 36} соответствует объектам структуры Person, у которых поле age равно 36. При этом значение поля name
будет помещаться в переменную name
Третий шаблон - Person{name, age} соответствует любой структуре Person. При этом значения ее полей будут помещаться в переменные name и age.
При этом названия переменных совпадают с названиями соответствующих полей структуры. Если названия переменных не совпадают, то мы должны явным образом указать,
как они соотносятся с полями структуры:
match tom{
Person{name: _username, age: 41} => println!("Name: Bob, age: 41"),
Person{name, age: 36} => println!("Name: {}, age: 36", name),
Person{name: person_name, age: person_age} => println!("Name: {}, age: {}", person_name, person_age)
}
Похожим образом можно использовать шаблоны для сравнения массивов:
fn main(){
let numbers = [1, 7, 3];
match numbers {
[1, 2, 3] => println!("Pattern1. [1, 2, 3]"),
[x, 2, 3] => println!("Pattern2. [{}, 2, 3]", x),
[x, y, 3] => println!("Pattern3. [{}, {}, 3]", x, y),
[x, y, z] => println!("Pattern4. [{}, {}, {}]", x, y, z),
}
}
Здесь конструкция match сравнивает массив numbers с набором шаблонов, которые содержат как конкретные значения, так и переменные. В
данном случае массив numbers соответствует третьему шаблону - [x, y, 3]. Этот шаблон соответствует любому массиву, третий элемент которого равен 3.
В случае с перечислениями шаблоны позволяют получить значения констант перечисления в переменные:
enum DayTime{
Morning(String),
Evening(String)
}
fn main(){
let date_time = DayTime::Morning("Доброе утро".to_string());
match date_time {
DayTime::Morning(message)=> println!("{}", message),
DayTime::Evening(message)=> println!("{}", message)
}
}
Перечисление DayTime определяет две константы, которые хранят значение типа String.
Конструкция match предусматривает для объекта перечисления два шаблона, в каждом из которых определяется переменная message. В эту переменную
мы можем получить значение константы.
В данном случае значение переменной date_time будет соответствовать шаблону DayTime::Morning(message). В итоге на консоль будет
выведено сообщение "Доброе утро".
При это константы перечисления могут представлять разные типы:
enum DayTime{
Morning(u8, u8),
Evening(String)
}
В данном случае константа Morning хранит сразу два значения типа u8 (условно первый и последний утренние часы). Соответственно при определении переменной необходимо передать два значения:
let dt = DayTime::Morning(0, 12);
Тогда шаблон для этой константы может предоставить две переменные - для каждого отдельного значения константы:
enum DayTime{
Morning(u8, u8),
Evening(String)
}
fn main(){
let morning = DayTime::Morning(0, 12);
match morning {
DayTime::Morning(6, end)=> println!("Утро: с 6 до {}", end),
DayTime::Morning(start, end)=> println!("Утренние часы: с {} до {}", start, end),
DayTime::Evening(message)=> println!("{}", message),
}
}
Если нам не нужны какие-то значения, вместо них также можно определить прочерк:
enum DayTime{
Morning(u8, u8),
Evening(String)
}
fn main(){
let morning = DayTime::Morning(5, 12);
match morning {
DayTime::Morning(_, end)=> println!("Утренние часы заканчиваются в {} часов", end),
DayTime::Evening(message)=> println!("{}", message)
}
}
В данном случае мы не используем первое значение константы DayTime::Morning, однако для каждого значения константы нам надо что-то указать, например, конкретное значение или имя переменной.
А прочерк позволяет выйти из этой ситуации и вообще не использовать это значение.
То же самое можно проделать и в шаблонах для других типов. Например, для структур:
struct Person { name: String, age: u8, height: f32 }
fn main(){
let bob = Person{
name: "Bob".to_string(),
age: 33,
height: 1.70
};
match bob {
Person{name, age, height: _} => println!("name: {} age: {}", name, age)
}
}
В данном случае мы не собираемся использовать в конструкции match поле height, поэтому в шаблоне для этого поля указывается прочерк: Person{name, age, height: _}.
Пример с кортежами:
fn main(){
let user = ("Tom", 36);
match user {
("Bob", age) => println!("Name: Bob, age: {}", age),
(name, _) => println!("Name: {}", name)
}
}
Если структура, кортеж, перечисление или массив содержат много значений подряд, которые нам не нужны и мы хотим их пропустить, то можно использовать оператор ... Например:
struct Person { name: String, age: u8, height: f32 }
fn main(){
let bob = Person{
name: "Bob".to_string(),
age: 33,
height: 1.70
};
match bob {
Person{name, ..} => println!("name: {}", name)
}
}
Шаблон Person{name, ..} позволяет получить значение поля name, остальные поля структуры игнорируются.
Другой пример - получим только первый и последний элементы массива:
fn main(){
let numbers = [2, 3, 4, 5, 6, 7, 8];
match numbers {
[first,.., last] => println!("first: {}, last: {}", first, last)
}
}
С помощью символа @ можно определить переменные при совпоставлении шаблонов в виде:
переменная @ паттерн
Например:
struct Person{
id: u32,
}
fn main(){
let tom = Person {id: 5};
let bob = Person {id: 28};
print_id(&tom); // id: 5
print_id(&bob); // Undefined
}
fn print_id(p: &Person){
match p {
Person{id: person_id @ 1..=10} => println!("id: {person_id}"),
_ => println!("Undefined"),
}
}
В данном случае в функции print_id с помощью конструкции match объект Person сопоставляется с одним из двух шаблонов. В первом шаблоне определяется переменная person_id:
Person{id: person_id @ 1..=10}
Здесь, если значение поля id попадает в диапазон от 1 до 10 включительно, то значение id передается в переменную person_id.
С помощью выражения if мы можем добавлять к шаблон дополнительные условия, которым они должны соответствовать:
struct Person{
name: String,
age: u8,
}
fn main(){
let tom = Person {name: String::from("Tom"), age: 22};
let bob = Person {name: String::from("Bob"), age: 44};
print_person(&tom); // Person Tom (young)
print_person(&bob); // Person Bob
}
fn print_person(p: &Person){
match p {
Person{name, age} if age >=&18 && age <=&30 => println!("Person {name} (young)"),
Person{name, ..} => println!("Person {name}"),
}
}
Здесь в функции print_person сопоставляет объект Person с двумя шаблонами. В первом шаблоне используется условное выражение if:
Person{name, age} if age >=&18 && age <=&30 => .......
То есть объект успешно сопоставляется с шаблоном, если значение age находится в некотором диапазоне. Иначе объект Person будет сопоставляться со вторым шаблоном.