Pull to refresh

Rust: & и ref в паттернах

Reading time5 min
Views11K
Original author: Karol Kuczmarski
(продолжение)

Как вам, думаю, известно, Раст входит в число языков реализующих сопоставление с образцом (pattern matching). В случае если вам незнаком данный термин, вы можете думать о нём как об обобщёном switch выражении в котором мы сравниваем объекты не только по значению, но и по структуре:

match hashmap.get(&key) {
    Some(value) => do_something_with(value),
    None => { panic!("Oh noes!"); },
}

Разумеется сравнением дело не ограничивается. Как вы можете видеть в примере выше, объекты так же могут быть деструктурированы во время сопоставления (Some(value)) и их части присвоены другим переменным (value), которые могут быть далее использованы в соответствующей ветви match выражения.

Изящно, не правда ли? В Расте сопоставление с образцом это хлеб и масло не только для match, но и для for, (if) let и даже для обыкновенных аргументов функции.

Однако, долгое время я достаточно смутно представлял себе что происходит в случае когда мы добавляем к сопоставлению ссылки и заимствования. & и ref являются двумя «операторами» часто используемых при этом. Разнице между ними и будет посвящена данная статья.

Вероятно вы уже достаточно хорошо знакомы с первым «оператором», используемым для создания ссылок и типов со ссылками. ref тоже весьма прозрачно намекает на связь со ссылками. Тем не менее эти два выражения играют очень разные роли при использовании внутри сопоставления.

Дабы добавить путаницы достаточно часто они используются вместе:

use hyper::Url;

// печатает параметры запроса заданного URL адреса
let url = Url::parse(some_url).unwrap();
let query_params: Vec<(String, String)> = url.query_pairs().unwrap_or(vec![]);
for &(ref name, ref value) in &query_params {
    println!("{}={}", name, value);
}

Отсутствие одного из них в лучшем случае будет отмечено компилятором вместе с предложением куда следует добавить то или иное выражение, что разумеется случается далеко не всегда. Поэтому весьма желательно понимать что здесь происходит на самом деле.

Часть ссылки и часть сопоставления


Раст очень гибок в части того какие значения могут быть частью сопоставления. Вам придётся приложить недюжие усилия что бы найти нечто, что не может быть частью match выражения. И объекты, и ссылки на них могу быть без проблем использованы в нём:

struct Foo(i32);
// ...
let foo = &Foo(42);
match foo {
    x => println!("Matched!"),
}

Однако, в предыдущем примере нам обычно неинтересна сама ссылка, но то куда она указывает:

match foo {
    &Foo(num) => println!("Matched with number {}", num),
}

Как вы видите именно для этого и применяется амперсанд. Так же как и типы (Some, Ok или вышеобозначенный Foo), & указывает на то какое значение мы ожидаем при сопоставлении. Видя амперсанд программа знает, что мы хотим сопоставить ссылки на некие объекты, а не сами объекты.

Но почему это различие между объектом и ссылкой на него так важно? В других местах Раст достаточно гибок в размытии границы между ссылками и самими объектами, например в случае вызова методов объекта. (механизм для этого называется "преобразованием при разыменовании" или по английски "Deref coercion" )

Однако, сопоставление с образцом, из-за возможности распаковки значений на их составные части, является деструктивной операцией. Всё к чему мы применим match (или иное похожее выражение) будет по умолчанию перемещено в данный блок:

let maybe_name = Some(String::from("Alice"));
// ...
match maybe_name {
    Some(n) => println!("Hello, {}", n),
    _ => {},
}
do_something_with(maybe_name)

В полном соответствии с семантикой владения, match выражение предотвратит последующие попытки перемещения и в сущности поглотит объект. Таким образом, код выше выдаст следующее сообщение об ошибке:

error: use of partially moved value: `maybe_name` [E0382]
    do_something_with(maybe_name);
                      ^~~~~~~~~~

Так что как и Some амперсанд в сущности просто часть паттерна с которым мы производим сопоставление. И так же как и с Some и другими типами, мы имеет очевидную симметрию: если мы использовали & для создания значения, нам необходимо будет использовать амперсанд и для его распаковки.

Синтаксис используемый в паттернах для распаковки объекта аналогичен использованному при его создании


Предотвращаем перемещение


Ошибки, аналогичные приведённой выше, часто содержат полезные заметки:

note: `(maybe_name:core::option::Option::Some).0` moved here because it has type `collections::string::String`, which is moved by default
         Some(n) => println!("Hello, {}", n),
              ^

И пути потенциального исправления ошибки:

help: if you would like to borrow the value instead, use a `ref` binding as shown:
        Some(ref n) => println!("Hello, {}", n),

Именно здесь и выходит на сцену ref.

Сообщение говорит нам, что если мы добавим ключевое слово ref в предложенное место, мы сменим перемещение на заимствование для переменной использованной в данном сопоставлении. (в нашем случае это n) Как и раньше данная переменная захватит необходимое значение, но на этот раз без владения им.

И это различие чрезвычайно важно.

В отличии от амперсанда, ref не обозначает нечто с чем мы производим сопоставление. Оно никоим образом не влияет на то будет ли значение сопоставлено с данным паттерном или нет.

Единственное, что оно меняет это то как части сопоставленного значения будут захвачены переменными паттерна:

  • По умолчанию без ref они перемещаются в ветвь match
  • При наличии ref, они заимствуются и представляются в качестве ссылок

В нашем примере переменная n в паттерне Some(n) имеет тип String, т.е. тот же самый тип что и в сопоставленной структуре. В противоположность этому, другая n в паттерне Some(ref n) имеет тип &String, т.е. ссылку на поле объекта.

Первое это перемещение, второе это заимствование.

Ключевое слово ref обозначает что переменная внутри паттерна сопоставления должна заимствовать значение, а не перемещать его. Это не является частью паттерна с точки зрения непосредственно сопоставления.

Используем их вместе


Давайте теперь разберём что именно происходит в том запутанном примере из начала статьи:

for &(ref name, ref value) in &query_params {
    println!("{}={}", name, value);
}

Т.к. мы знаем что ref никоим образом не влияет на сопоставится ли паттерн или нет, мы могли бы просто вставить что-то вроде &(a, b), данный вариант значительно читабельнее: ясно видно, что мы ожидаем ссылку на объект являющийся кортежем из двух элементов. Разумеется именно такие кортежи и являются членами вектора по которому происходит итерирование.

Проблема в том, что без ref-ов мы пытаемся переместить значения кортежа внутрь цикла, но т.к. мы итерируем по &query_params, мы лишь заимствуем каждый из кортежей, поэтому такое перемещение на самом деле невозможно. По сути, это было бы классической попыткой перемещения значения из контекста заимствования.

Но данное перемещение нам и не нужно. Единственное, что наш цикл делает, так это печатает значения, поэтому доступа к ним через ссылки будет более чем достаточно.

И это как раз то, что ref нам даёт. Вернув обратно данное ключевое слово мы перейдём от перемещения к заимствованию.

Суммируя


  • & обозначает что паттерн ожидает ссылку на объект. Таким образом & является частью паттерна. Отсюда &Foo будет сопоставляться иначе чем Foo
  • ref отмечает что вы хотите получить ссылку на распакованное значение. Оно не участвует непосредственно в сопоставлении паттерна. Поэтому Foo(ref foo) будет сопоставлятся тому же объекту что и Foo(foo)
Tags:
Hubs:
Total votes 48: ↑47 and ↓1+46
Comments2

Articles