Извлечение захваченных значений из замыканий и трейты Fn
После того, как замыкание захватывает ссылку или получает владение значением из окружающей среды, где оно определено (влияние на то, что, если что-то, перемещается в замыкание), код в теле замыкания определяет, что происходит с ссылками или значениями, когда замыкание оценивается позже (влияние на то, что, если что-то, перемещается из замыкания).
Тело замыкания может делать следующее: перемещать захваченное значение из замыкания, изменять захваченное значение, ни перемещать, ни изменять значение или вообще не захватывать ничего из окружающей среды.
Способ, которым замыкание захватывает и обрабатывает значения из окружающей среды, влияет на то, какие трейты реализует замыкание, и трейты - это то, как функции и структуры могут указать, какие виды замыканий они могут использовать. Замыкания автоматически реализуют один, два или все три из этих трейтов Fn
в накапливающем порядке, в зависимости от того, как тело замыкания обрабатывает значения:
FnOnce
применяется к замыканиям, которые можно вызвать один раз. Все замыкания реализуют по крайней мере этот трейт, потому что все замыкания можно вызвать. Замыкание, которое перемещает захваченные значения из своего тела, будет реализовывать только FnOnce
и ни один из других трейтов Fn
, потому что оно может быть вызвано только один раз.
FnMut
применяется к замыканиям, которые не перемещают захваченные значения из своего тела, но могут изменять захваченные значения. Эти замыкания можно вызывать более одного раза.
Fn
применяется к замыканиям, которые не перемещают захваченные значения из своего тела и не изменяют захваченные значения, а также к замыканиям, которые не захватывают ничего из своей окружающей среды. Эти замыкания можно вызывать более одного раза без изменения их окружающей среды, что важно в таких случаях, как вызов замыкания несколько раз одновременно.
Посмотрим на определение метода unwrap_or_else
для Option<T>
, которое мы использовали в Листинге 13-1:
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
Помните, что T
- это обобщенный тип, представляющий тип значения в варианте Some
перечисления Option
. Этот тип T
также является типом возвращаемым функцией unwrap_or_else
: код, который вызывает unwrap_or_else
для Option<String>
, например, получит String
.
Далее, обратите внимание, что функция unwrap_or_else
имеет дополнительный обобщенный параметр типа F
. Тип F
- это тип параметра с именем f
, который является замыканием, которое мы предоставляем при вызове unwrap_or_else
.
Ограничение трейта, указанное для обобщенного типа F
, - это FnOnce() -> T
, что означает, что F
должен быть вызываемым один раз, не принимать аргументов и возвращать T
. Использование FnOnce
в ограничении трейта выражает ограничение, что unwrap_or_else
будет вызывать f
максимум один раз. В теле unwrap_or_else
мы можем увидеть, что если Option
равно Some
, f
не будет вызван. Если Option
равно None
, f
будет вызван один раз. Поскольку все замыкания реализуют FnOnce
, unwrap_or_else
принимает самую широкую совокупность замыканий и является максимально гибким.
Примечание: Функции также могут реализовать все три трейта Fn
. Если то, что мы хотим сделать, не требует захвата значения из окружающей среды, мы можем использовать имя функции вместо замыкания, где нам нужен тип, реализующий один из трейтов Fn
. Например, для значения Option<Vec<T>>
мы могли бы вызвать unwrap_or_else(Vec::new)
, чтобы получить новый пустой вектор, если значение равно None
.
Теперь посмотрим на метод стандартной библиотеки sort_by_key
, определенный для срезов, чтобы понять, как он отличается от unwrap_or_else
и почему sort_by_key
использует FnMut
вместо FnOnce
для ограничения трейта. Замыкание получает один аргумент в виде ссылки на текущий элемент в срезе, который рассматривается, и возвращает значение типа K
, которое можно упорядочить. Эта функция полезна, когда вы хотите отсортировать срез по определенному атрибуту каждого элемента. В Листинге 13-7 у нас есть список экземпляров Rectangle
, и мы используем sort_by_key
, чтобы отсортировать их по атрибуту width
от меньшего к большему.
Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
list.sort_by_key(|r| r.width);
println!("{:#?}", list);
}
Листинг 13-7: Использование sort_by_key
для сортировки прямоугольников по ширине
Этот код выводит:
[
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
Rectangle {
width: 10,
height: 1,
},
]
Причина, по которой sort_by_key
определен для приема замыкания FnMut
, заключается в том, что он вызывает замыкание несколько раз: один раз для каждого элемента в срезе. Замыкание |r| r.width
не захватывает, не изменяет и не перемещает ничего из своей окружающей среды, поэтому оно соответствует требованиям ограничения трейта.
В отличие от этого, Листинг 13-8 показывает пример замыкания, которое реализует только трейт FnOnce
, потому что оно перемещает значение из окружающей среды. Компилятор не позволит нам использовать это замыкание с sort_by_key
.
Filename: src/main.rs
--snip--
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut sort_operations = vec![];
let value = String::from("by key called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{:#?}", list);
}
Листинг 13-8: Проба использования замыкания FnOnce
с sort_by_key
Это искусственный, сложный способ (который не работает), чтобы попробовать подсчитать количество раз, когда sort_by_key
вызывается при сортировке list
. Этот код пытается сделать это подсчетом, добавляя value
- String
из окружающей среды замыкания - в вектор sort_operations
. Замыкание захватывает value
, а затем перемещает value
из замыкания, передавая владение value
вектору sort_operations
. Это замыкание можно вызвать один раз; попытка вызвать его вторично не сработает, потому что value
больше не будет в окружающей среде, чтобы снова добавить его в sort_operations
! Поэтому это замыкание реализует только FnOnce
. Когда мы пытаемся скомпилировать этот код, мы получаем ошибку, что value
не может быть перемещено из замыкания, потому что замыкание должно реализовать FnMut
:
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut`
closure
--> src/main.rs:18:30
|
15 | let value = String::from("by key called");
| ----- captured outer variable
16 |
17 | list.sort_by_key(|r| {
| ______________________-
18 | | sort_operations.push(value);
| | ^^^^^ move occurs because `value` has
type `String`, which does not implement the `Copy` trait
19 | | r.width
20 | | });
| |_____- captured by this `FnMut` closure
Ошибка указывает на строку в теле замыкания, которая перемещает value
из окружающей среды. Чтобы исправить это, мы должны изменить тело замыкания так, чтобы оно не перемещало значения из окружающей среды. Хранение счетчика в окружающей среде и увеличение его значения в теле замыкания - более простой способ подсчитать количество раз, когда вызывается sort_by_key
. Замыкание в Листинге 13-9 работает с sort_by_key
, потому что оно только захватывает изменяемую ссылку на счетчик num_sort_operations
и поэтому может быть вызван более одного раза.
Filename: src/main.rs
--snip--
fn main() {
--snip--
let mut num_sort_operations = 0;
list.sort_by_key(|r| {
num_sort_operations += 1;
r.width
});
println!(
"{:#?}, sorted in {num_sort_operations} operations",
list
);
}
Листинг 13-9: Использование замыкания FnMut
с sort_by_key
допускается.
Трейты Fn
важны при определении или использовании функций или типов, которые используют замыкания. В следующем разделе мы обсудим итераторы. Многие методы итераторов принимают аргументы-замыкания, поэтому помните об этих деталях замыканий, когда мы продолжаем!