Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Паттерн матчинг

В дополнение к оператору 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.