Паттерн матчинг
В дополнение к оператору if, в Rust имеется ещё один оператор ветвления, который мы не рассмотрели раньше — оператор match, который часто называют паттерн-матчингом (pattern matching).
Этот оператор является неким гибридом классического switch-case (из C или Java) и деструктурирующего присваивания.
Синтаксис:
match значение {
шаблон 1 => выражение 1,
…
шаблон N => выражение N,
}
Оператор последовательно примеряет шаблоны к значению: если шаблон совпадает, то выполняется соответствующее ему выражение и оператор match завершает свою работу, иначе происходит проверка следующего по списку шаблона.
Шаблон может быть:
- константой
- переменной или переменной с условным блоком
- шаблоном деструктурирующего присваивания
Рассмотрим все варианты в порядке возрастания сложности.
match как switch-case
Самый простой вариант использования оператора match — прямое сопоставление значения: как switch-case из C или Java.
В качестве примера, проверим равно ли значение числовой переменной 0 или 1:
fn main() {
let a = 1;
match a {
0 => println!("It is 0"),
1 => println!("It is 1"),
_ => println!("Neither 0 nor 1"),
}
// Напечатает: It is 1
}
Компилятор проверяет, что в теле match присутствуют ветки для всех возможных вариантов значения переменной.
Именно поэтому после веток с 0 и 1 мы также вставили ветку с шаблоном _. Этот шаблон совпадает с абсолютно любым значением и служит в качестве ветки default, если проводить аналогию с оператором switch-case, или в качестве ветки else, если сравнивать с оператором if.
На самом деле, шаблон _ — это обычная “выброшенная” переменная, такая же, как и те, что мы видели в деструктурирующих шаблонах. Вместо “выброшенной” переменной можно указать переменную с любым корректным именем, и тогда в неё будет записано значение, переданное в match.
fn main() {
let a = 5;
match a {
0 => println!("The number is 0"),
1 => println!("The number is 1"),
x => println!("The number is {x}"),
}
// Напечатает: The number is 5
}
Имейте в виду, что такой шаблон в виде переменной подходит под любое значение, поэтому должен располагаться в самом конце списка шаблонов.
В шаблоне можно также перечислить несколько значений:
fn main() {
let a: u32 = 7;
match a {
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 =>
println!("The number is less than 10"),
_ =>
println!("The number is equal to or greater than 10"),
}
// Напечатает: The number is less than 10
}
Проверяя числа в шаблоне, вместо того, чтобы перечислять подряд все значения, можно указать диапазон:
fn main() {
let a: u32 = 44;
match a {
0 ..= 9 => // от 0 до 9 включительно
println!("The number is less than 10"),
10 .. 100 => // от 10 до 100 не включительно
println!("The number is in range [10,99]"),
_ =>
println!("The number is equal to or greater than"),
}
// Напечатает: The number is in range [10,99]
}
Переменная привязка
В примере выше мы указали целый диапазон значений. Но что если мы хотим знать не только диапазон, в который попадает значение, но и само значение? Можно, конечно, напрямую обратиться к переменной, переданной в match, однако это не всегда возможно (например, если в match было передано арифметическое выражение или вызов функции).
В таком случае нам поможет переменная привзяка (binding). Это такая переменная, в которую записывается значение, которое удовлетворило шаблону.
Синтаксис:
привязка @ шаблон => выражение
Теперь перепишем пример выше с использованием привязок:
fn main() {
match 22 + 22 {
x@(0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9) =>
println!("{x} is less than 10"),
x@10 .. 100 =>
println!("{x} is in range [10,99]"),
x =>
println!("{x} is equal to or greater than"),
}
// Напечатает: 44 is in range [10,99]
}
match как выражение
Оператор match, как и оператор if, возвращает значение. Результат всего оператора match — результат отработавшей условной ветки.
Пример оператора match, который используется для получения модуля числа (неотрицательной части):
#![allow(unused)]
fn main() {
let a = -5;
let absolute = match a {
.. 0 => -a, // диапазон от минус бесконечности до 0 невключительно
_ => a,
};
println!("{absolute}"); // 5
}
Сравнение со строковыми литералами
Допустим, у нас имеется объект строки типа String и мы хотим проверить её содержимое при помощи оператора match. Мы можем попытаться написать такую проверку следующим образом:
fn main() {
let name = String::from("Robert Smith");
let is_anonymous = match name {
"Anonymous".to_string() => true,
"John Doe".to_string() => true,
_ => false,
};
}
Компиляция этой программы завершится с ошибкой:
"Anonymous".to_string() => true,
^^^^^^^^^^^^^^^^^^^^^^^ not a pattern
Дело в том, что, как мы сказали в самом начале, шаблон может быть либо константой, либо переменной, либо шаблоном деструктурирующего присваивания. Выражение "Anonymous".to_string() не является ни одним из вышеперечисленных.
Note
Здесь, говоря о переменной, мы имеем в виду переменную-шаблон для присваивания (или деструктурирующего присваивания), а не переменную, объявленную за пределами
matchблока и содержащую некое значение. Т.е. такой код работает не так, как вы могли ожидать:fn main() { let anonymous = "Anonymous".to_string(); let john_doe = "John Doe".to_string(); let name = String::from("Robert Smith"); let is_anonymous = match name { anonymous => true, // отработает эта ветка john_doe => true, _ => false, }; println!("{is_anonymous}"); // true }Эта программа печатает “true”, потому что переменная
anonymousв блокеmatchвоспринимается не как переменная объявленная выше и содержащая строку “Anonymous”, а как переменная-шаблон для присваивания. Разумеется, одиночная переменная без дополнительных условий является шаблоном, который подходит под абсолютно любое значение.
Единственное, что нам остаётся — использовать строковые литералы, так как они являются константами.
fn main() {
let name = String::from("Robert Smith");
let is_anonymous = match name.as_str() {
"Anonymous" | "John Doe" => true,
_ => false,
};
}
Обратите внимание, что мы передаём в оператор match не просто переменную name, а строковый слайс от неё — name.as_str().
match для слайсов
Для слайсов можно использовать шаблоны, которые выглядят в точности как шаблоны деструктурирующего присваивания для массивов.
fn main() {
let v = vec![1, 2, 3, 4, 5];
let s = match v.as_slice() {
[] => 0,
[a, b, c, ..] => a + b + c,
_ => -1,
};
println!("{}", s); // 6
}
Еще раз напомним, что слайсы нельзя использовать в “обычном” деструктурирующем присваивании, потому что компилятор не может гарантировать, что количество переменных из деструктурирующего шаблона соответствует количеству элементов в слайсе.
Но у match такой проблемы нет, так как несовпадение слайса с шаблоном, всего лишь означает, что match перейдёт к следующему шаблону и попытается сопоставить слайс с ним.
match для структур
Синтаксис шаблонов для структур в паттерн-матчинге очень похож на синтаксис шаблонов для деструктурирующего присваивания, однако позволяет дополнительно задавать значения для полей.
struct Person { name: String, age: u32 }
fn main () {
let p = Person { name: String::from("John"), age: 17 };
match p {
Person { name, age: 1 .. 18 } => println!("Person {name} is not adult"),
Person { name, age: 18 } => println!("Person {name} just turned 18"),
Person { name, .. } => println!("Person {name} is adult"),
}
// Напечатает: Person John is not adult
}
match if
В шаблонах можно указывать дополнительное условие при помощи ключевого слова if.
struct Person { name: String, age: u32 }
fn main () {
let p = Person { name: String::from("John"), age: 17 };
match p {
Person { name, age } if age < 18 =>
println!("Person {name} is not adult"),
Person { name, age } if age == 18 =>
println!("Person {name} just turned 18"),
Person { name, .. } =>
println!("Person {name} is adult"),
}
// Напечатает: Person John is not adult
}
Условие в блоке if может:
- быть составным, т.е. содержать несколько условий, скомбинированных через операторы
||и&& - обращаться к полям шаблона, биндингам и любым другим именам, доступным в этом скоупе
При помощи блока if можно переписать наш пример со строками:
fn main() {
let name = String::from("Robert Smith");
let is_anonymous = match name {
s if s == "Anonymous" => true,
s if s == "John Doe" => true,
_ => false,
};
println!("{is_anonymous}"); // false
}
Такой код работает корректно, однако вариант с сопоставлением со строковыми литералами более изящен.
ref
Как мы знаем, деструктурирующее присваивание разрушает присваиваемый объект. Если мы не хотим разрушать объект, а хотим просто получить ссылку на его поле, то мы можем “деструктурировать” объект не по значению, а по ссылке:
#[derive(Debug)]
struct Person { name: String, age: u32 }
fn main() {
let mut person = Person { name: String::from("Anonymous"), age: 25 };
let Person {name, ..} = &mut person;
*name = "John Doe".to_string();
// Объект person всё еще "жив"
println!("{person:?}"); // Person { name: "John Doe", age: 25 }
}
Деструктурирующие шаблоны в операторе match — не исключение: они так же уничтожают переданный в них объект.
Но можно ли передать объект в оператор match по ссылке так же, как в “обычном” деструктурирующем присваивании?
Да, можно:
#[derive(Debug)]
struct Person { name: String, age: u32 }
fn main () {
let mut person = Person { name: String::from("Anonymous"), age: 25 };
match &mut person {
Person { name, .. } if name == "Anonymous" => {
*name = "John Doe".to_string();
},
Person { .. } => (),
}
println!("{person:?}"); // Person { name: "John Doe", age: 25 }
}
Однако специально для патерн-матчинга существует ключевое слово ref, которое позволяет в деструктурирующем шаблоне захватить поле объекта по ссылке несмотря на то, что объект был передан в match по значению. При этом, если все поля объекта захватываются по ref ссылке (или игнорируются), то исходный объект не разрушается.
#[derive(Debug)]
struct Person { name: String, age: u32 }
fn main () {
let mut person = Person { name: String::from("Anonymous"), age: 25 };
match person {
Person { ref mut name, .. } if name == "Anonymous" => {
*name = "John Doe".to_string();
},
Person { .. } => (),
}
println!("{person:?}"); // Person { name: "John Doe", age: 25 }
}
Как видите, здесь мы передали переменную person в match по значению, а не по ссылке. Но поскольку в шаблоне мы обратились к полю name по ref ссылке, а не по значению, уничтожение объекта не произошло. Поэтому переменная person осталась действительной после передачи в match.