Типы замыканий
Выражение замыкания производит значение замыкания с уникальным, анонимным типом, который не может быть записан. Тип замыкания приблизительно эквивалентен структуре, содержащей захваченные значения. Например, следующее замыкание:
#![allow(unused)] fn main() { #[derive(Debug)] struct Point { x: i32, y: i32 } struct Rectangle { left_top: Point, right_bottom: Point } fn f<F : FnOnce() -> String> (g: F) { println!("{}", g()); } let mut rect = Rectangle { left_top: Point { x: 1, y: 1 }, right_bottom: Point { x: 0, y: 0 } }; let c = || { rect.left_top.x += 1; rect.right_bottom.x += 1; format!("{:?}", rect.left_top) }; f(c); // Печатает "Point { x: 2, y: 1 }". }
генерирует тип замыкания примерно следующим образом:
// Примечание: Это не точный перевод, это только для
// иллюстрации.
struct Closure<'a> {
left_top : &'a mut Point,
right_bottom_x : &'a mut i32,
}
impl<'a> FnOnce<()> for Closure<'a> {
type Output = String;
extern "rust-call" fn call_once(self, args: ()) -> String {
self.left_top.x += 1;
*self.right_bottom_x += 1;
format!("{:?}", self.left_top)
}
}
так что вызов f работает так, как если бы это было:
f(Closure{ left_top: &mut rect.left_top, right_bottom_x: &mut rect.right_bottom.x });
Режимы захвата
Режим захвата определяет, как выражение-место из окружения заимствуется или перемещается в замыкание. Режимы захвата:
- Неизменяемое заимствование (
ImmBorrow) — Выражение-место захватывается как разделяемая ссылка. - Уникальное неизменяемое заимствование (
UniqueImmBorrow) — Это похоже на неизменяемое заимствование, но должно быть уникальным, как описано ниже. - Изменяемое заимствование (
MutBorrow) — Выражение-место захватывается как изменяемая ссылка. - Перемещение (
ByValue) — Выражение-место захватывается перемещением значения в замыкание.
Выражения-места из окружения захватываются с первого режима, который совместим с тем, как захваченное значение используется внутри тела замыкания. Режим не зависит от кода, окружающего замыкание, такого как времена жизни задействованных переменных или полей, или самого замыкания.
Значения Copy
Значения, которые реализуют Copy и перемещаются в замыкание, захватываются в режиме ImmBorrow.
#![allow(unused)] fn main() { let x = [0; 1024]; let c = || { let y = x; // x захвачен через ImmBorrow }; }
Захват входных аргументов в async
Асинхронные замыкания всегда захватывают все входные аргументы, независимо от того, используются ли они внутри тела.
Точность захвата
Путь захвата — это последовательность, начинающаяся с переменной из окружения, за которой следует ноль или более проекций мест, примененных к этой переменной.
Проекция места — это доступ к полю, индекс кортежа, разыменование (и автоматические разыменования) или индекс массива или среза, примененный к переменной.
Замыкание заимствует или перемещает путь захвата, который может быть усечен на основе правил, описанных ниже.
Например:
#![allow(unused)] fn main() { struct SomeStruct { f1: (i32, i32), } let s = SomeStruct { f1: (1, 2) }; let c = || { let x = s.f1.1; // s.f1.1 захвачен через ImmBorrow }; c(); }
Здесь путь захвата — это локальная переменная s, за которой следует доступ к полю .f1, а затем индекс кортежа .1.
Это замыкание захватывает неизменяемое заимствование s.f1.1.
Общий префикс
В случае, когда путь захвата и один из предков этого пути оба захватываются замыканием, путь-предок захватывается с наивысшим режимом захвата среди двух захватов, CaptureMode = max(AncestorCaptureMode, DescendantCaptureMode), используя строго слабое упорядочение:
ImmBorrow < UniqueImmBorrow < MutBorrow < ByValue
Заметьте, что это может потребовать применения рекурсивно.
#![allow(unused)] fn main() { // В этом примере есть три разных пути захвата с общим предком: fn move_value<T>(_: T){} let s = String::from("S"); let t = (s, String::from("T")); let mut u = (t, String::from("U")); let c = || { println!("{:?}", u); // u захвачен через ImmBorrow u.1.truncate(0); // u.0 захвачен через MutBorrow move_value(u.0.0); // u.0.0 захвачен через ByValue }; c(); }
В целом это замыкание захватит u через ByValue.
Усечение по самой правой разделяемой ссылке
Путь захвата усекается на самом правом разыменовании в пути захвата, если разыменование применяется к разделяемой ссылке.
Это усечение разрешено, потому что поля, читаемые через разделяемую ссылку, всегда будут читаться через разделяемую ссылку или копию. Это помогает уменьшить размер захвата, когда дополнительная точность не дает никакой выгоды с точки зрения проверки заимствований.
Причина, по которой это самое правое разыменование, заключается в том, чтобы помочь избежать более короткого времени жизни, чем необходимо. Рассмотрим следующий пример:
#![allow(unused)] fn main() { struct Int(i32); struct B<'a>(&'a i32); struct MyStruct<'a> { a: &'static Int, b: B<'a>, } fn foo<'a, 'b>(m: &'a MyStruct<'b>) -> impl FnMut() + 'static { let c = || drop(&m.a.0); c } }
Если бы это захватывало m, то замыкание больше не переживало бы 'static, поскольку m ограничено 'a. Вместо этого оно захватывает (*(*m).a) через ImmBorrow.
Привязки с подстановочным образцом
Замыкания захватывают только данные, которые нужно прочитать.
Привязка значения с подстановочным образцом не считается чтением и, следовательно, не будет захвачена.
Например, следующие замыкания не захватят x:
#![allow(unused)] fn main() { let x = String::from("hello"); let c = || { let _ = x; // x не захвачен }; c(); let c = || match x { // x не захвачен _ => println!("Hello World!") }; c(); }
Это также включает деструктуризацию кортежей, структур и перечислений. Поля, сопоставленные с RestPattern или StructPatternEtCetera, также не считаются прочитанными, и поэтому эти поля не будут захвачены. Следующее иллюстрирует некоторые из них:
#![allow(unused)] fn main() { let x = (String::from("a"), String::from("b")); let c = || { let (first, ..) = x; // захватывает `x.0` через ByValue }; // Первое поле кортежа было перемещено в замыкание. // Второе поле кортежа все еще доступно. println!("{:?}", x.1); c(); }
#![allow(unused)] fn main() { struct Example { f1: String, f2: String, } let e = Example { f1: String::from("first"), f2: String::from("second"), }; let c = || { let Example { f2, .. } = e; // захватывает `e.f2` через ByValue }; // Поле f2 не может быть доступно, так как оно перемещено в замыкание. // Поле f1 все еще доступно. println!("{:?}", e.f1); c(); }
Частичные захваты массивов и срезов не поддерживаются; весь срез или массив всегда захватывается, даже если используется с подстановочным сопоставлением с образцом, индексацией или под-срезами. Например:
#![allow(unused)] fn main() { #[derive(Debug)] struct Example; let x = [Example, Example]; let c = || { let [first, _] = x; // захватывает весь `x` через ByValue }; c(); println!("{:?}", x[1]); // ОШИБКА: заимствование перемещенного значения: `x` }
Значения, сопоставленные с подстановочными символами, все равно должны быть инициализированы.
#![allow(unused)] fn main() { let x: i32; let c = || { let _ = x; // ОШИБКА: использованная привязка `x` не инициализирована }; }
Захват ссылок в контекстах перемещения
Поскольку не разрешено перемещать поля из ссылки, замыкания move будут захватывать только префикс пути захвата, который идет до, но не включая, первое разыменование ссылки.
Сама ссылка будет перемещена в замыкание.
#![allow(unused)] fn main() { struct T(String, String); let mut t = T(String::from("foo"), String::from("bar")); let t_mut_ref = &mut t; let mut c = move || { t_mut_ref.0.push_str("123"); // захватывает `t_mut_ref` через ByValue }; c(); }
Разыменование сырого указателя
Поскольку разыменование сырого указателя unsafe, замыкания будут захватывать только префикс пути захвата, который идет до, но не включая, первое разыменование сырого указателя.
#![allow(unused)] fn main() { struct T(String, String); let t = T(String::from("foo"), String::from("bar")); let t_ptr = &t as *const T; let c = || unsafe { println!("{}", (*t_ptr).0); // захватывает `t_ptr` через ImmBorrow }; c(); }
Поля объединений
Поскольку доступ к полю объединения unsafe, замыкания будут захватывать только префикс пути захвата, который идет до самого объединения.
#![allow(unused)] fn main() { union U { a: (i32, i32), b: bool, } let u = U { a: (123, 456) }; let c = || { let x = unsafe { u.a.0 }; // захватывает `u` через ByValue }; c(); // Это также включает запись в поля. let mut u = U { a: (123, 456) }; let mut c = || { u.b = true; // захватывает `u` через MutBorrow }; c(); }
Ссылка на невыровненные struct
Поскольку создание ссылок на невыровненные поля в структуре является неопределенным поведением,
замыкания будут захватывать только префикс пути захвата, который идет до, но не включая, первый доступ к полю в структуре, которая использует представление packed.
Это включает все поля, даже выровненные, чтобы защититься от проблем совместимости, если какие-либо поля в структуре изменятся в будущем.
#![allow(unused)] fn main() { #[repr(packed)] struct T(i32, i32); let t = T(2, 5); let c = || { let a = t.0; // захватывает `t` через ImmBorrow }; // Копирование из `t` допустимо. let (a, b) = (t.0, t.1); c(); }
Аналогично, взятие адреса невыровненного поля также захватывает всю структуру:
#![allow(unused)] fn main() { #[repr(packed)] struct T(String, String); let mut t = T(String::new(), String::new()); let c = || { let a = std::ptr::addr_of!(t.1); // захватывает `t` через ImmBorrow }; let a = t.0; // ОШИБКА: нельзя переместить из `t.0`, так как оно заимствовано c(); }
но вышеприведенное работает, если оно не упаковано, поскольку захватывает поле точно:
#![allow(unused)] fn main() { struct T(String, String); let mut t = T(String::new(), String::new()); let c = || { let a = std::ptr::addr_of!(t.1); // захватывает `t.1` через ImmBorrow }; // Перемещение здесь разрешено. let a = t.0; c(); }
Box против других реализаций Deref
Реализация трейта Deref для Box обрабатывается иначе, чем другие реализации Deref, поскольку она считается особой сущностью.
Например, рассмотрим примеры с Rc и Box. *rc десугарируется в вызов метода трейта deref, определенного на Rc, но поскольку *box обрабатывается иначе, можно точно захватить содержимое Box.
Box с не-move замыканием
В не-move замыкании, если содержимое Box не перемещается в тело замыкания, содержимое Box точно захватывается.
#![allow(unused)] fn main() { struct S(String); let b = Box::new(S(String::new())); let c_box = || { let x = &(*b).0; // захватывает `(*b).0` через ImmBorrow }; c_box(); // Сравните `Box` с другим типом, реализующим Deref: let r = std::rc::Rc::new(S(String::new())); let c_rc = || { let x = &(*r).0; // захватывает `r` через ImmBorrow }; c_rc(); }
Однако, если содержимое Box перемещается в замыкание, то весь box захватывается. Это делается для того, чтобы минимизировать объем данных, которые нужно переместить в замыкание.
#![allow(unused)] fn main() { // Это тот же пример, что и выше, за исключением того, что замыкание // перемещает значение вместо взятия ссылки на него. struct S(String); let b = Box::new(S(String::new())); let c_box = || { let x = (*b).0; // захватывает `b` через ByValue }; c_box(); }
Box с move замыканием
Аналогично перемещению содержимого Box в не-move замыкании, чтение содержимого Box в move замыкании захватит весь Box.
#![allow(unused)] fn main() { struct S(i32); let b = Box::new(S(10)); let c_box = move || { let x = (*b).0; // захватывает `b` через ByValue }; }
Уникальные неизменяемые заимствования в захватах
Захваты могут происходить через особый вид заимствования, называемый уникальным неизменяемым заимствованием, который не может быть использован где-либо еще в языке и не может быть записан явно. Оно происходит при изменении референта изменяемой ссылки, как в следующем примере:
#![allow(unused)] fn main() { let mut b = false; let x = &mut b; let mut c = || { // ImmBorrow и MutBorrow `x`. let a = &x; *x = true; // `x` захвачен через UniqueImmBorrow }; // Следующая строка является ошибкой: // let y = &x; c(); // Однако следующее допустимо. let z = &x; }
В этом случае заимствование x изменяемо невозможно, потому что x не mut.
Но в то же время заимствование x неизменяемо сделало бы присваивание незаконным,
потому что ссылка & &mut может быть не уникальной, поэтому ее нельзя безопасно использовать для изменения значения.
Поэтому используется уникальное неизменяемое заимствование: оно заимствует x неизменяемо, но, как и изменяемое заимствование, оно должно быть уникальным.
В приведенном выше примере раскомментирование объявления y приведет к ошибке, потому что это нарушит уникальность заимствования x замыканием; объявление z допустимо, потому что время жизни замыкания истекло в конце блока, освободив заимствование.
Трейты вызова и приведения
Типы замыканий все реализуют FnOnce, указывая, что они могут быть вызваны один раз
путем потребления владения замыканием. Кроме того, некоторые замыкания реализуют
более специфичные трейты вызова:
- Замыкание, которое не перемещает никакие захваченные переменные, реализует
FnMut, указывая, что оно может быть вызвано по изменяемой ссылке.
- Замыкание, которое не изменяет и не перемещает никакие захваченные переменные,
реализует
Fn, указывая, что оно может быть вызвано по разделяемой ссылке.
Note
Замыкания
moveвсе равно могут реализовыватьFnилиFnMut, даже если они захватывают переменные перемещением. Это связано с тем, что трейты, реализуемые типом замыкания, определяются тем, что замыкание делает с захваченными значениями, а не тем, как оно их захватывает.
Незахватывающие замыкания — это замыкания, которые ничего не захватывают из своего
окружения. Не-асинхронные, незахватывающие замыкания могут быть приведены к указателям на функции (например, fn())
с соответствующей сигнатурой.
#![allow(unused)] fn main() { let add = |x, y| x + y; let mut x = add(5,7); type Binop = fn(i32, i32) -> i32; let bo: Binop = add; x = bo(5,7); }
Трейты асинхронных замыканий
Асинхронные замыкания имеют дополнительное ограничение на то, реализуют ли они FnMut или Fn.
Future, возвращаемый асинхронным замыканием, имеет схожие характеристики захвата, как замыкание. Он захватывает выражения-места из асинхронного замыкания на основе того, как они используются. Говорят, что асинхронное замыкание одалживает своему Future, если оно имеет любое из следующих свойств:
Futureвключает изменяемый захват.- Асинхронное замыкание захватывает по значению, за исключением случаев, когда значение доступно с проекцией разыменования.
Если асинхронное замыкание одалживает своему Future, то FnMut и Fn не реализуются. FnOnce всегда реализуется.
Пример: Первое условие для изменяемого захвата можно проиллюстрировать следующим:
#![allow(unused)] fn main() { fn takes_callback<Fut: Future>(c: impl FnMut() -> Fut) {} fn f() { let mut x = 1i32; let c = async || { x = 2; // x захвачен через MutBorrow }; takes_callback(c); // ОШИБКА: асинхронное замыкание не реализует `FnMut` } }Второе условие для обычного захвата по значению можно проиллюстрировать следующим:
#![allow(unused)] fn main() { fn takes_callback<Fut: Future>(c: impl Fn() -> Fut) {} fn f() { let x = &1i32; let c = async move || { let a = x + 2; // x захвачен через ByValue }; takes_callback(c); // ОШИБКА: асинхронное замыкание не реализует `Fn` } }Исключение второго условия можно проиллюстрировать использованием разыменования, которое позволяет реализовать
FnиFnMut:#![allow(unused)] fn main() { fn takes_callback<Fut: Future>(c: impl Fn() -> Fut) {} fn f() { let x = &1i32; let c = async move || { let a = *x + 2; }; takes_callback(c); // OK: реализует `Fn` } }
Асинхронные замыкания реализуют AsyncFn, AsyncFnMut и AsyncFnOnce аналогично тому, как обычные замыкания реализуют Fn, FnMut и FnOnce; то есть в зависимости от использования захваченных переменных в его теле.
Другие трейты
Все типы замыканий реализуют Sized. Кроме того, типы замыканий реализуют
следующие трейты, если это разрешено типами захватов, которые они хранят:
Правила для Send и Sync соответствуют таковым для обычных структурных типов, в то время как
Clone и Copy ведут себя так, как если бы они были выведены. Для Clone порядок
клонирования захваченных значений не указан.
Поскольку захваты часто происходят по ссылке, возникают следующие общие правила:
- Замыкание
Sync, если все захваченные значенияSync. - Замыкание
Send, если все значения, захваченные по не-уникальной неизменяемой ссылке, являютсяSync, и все значения, захваченные по уникальной неизменяемой или изменяемой ссылке, копированием или перемещением, являютсяSend. - Замыкание
CloneилиCopy, если оно не захватывает никакие значения по уникальной неизменяемой или изменяемой ссылке, и если все значения, которые оно захватывает копированием или перемещением, являютсяCloneилиCopyсоответственно.
Порядок сброса
Если замыкание захватывает поле составных типов, таких как структуры, кортежи и перечисления, по значению, время жизни поля теперь будет привязано к замыканию. Как следствие, возможно, что несвязные поля составных типов будут сброшены в разное время.
#![allow(unused)] fn main() { { let tuple = (String::from("foo"), String::from("bar")); // --+ { // | let c = || { // ----------------------------+ | // tuple.0 захвачен в замыкание | | drop(tuple.0); // | | }; // | | } // 'c' и 'tuple.0' сброшены здесь ------------+ | } // tuple.1 сброшен здесь -----------------------------+ }
Редакция 2018 и ранее
Разница в типах замыканий
В Редакции 2018 и ранее замыкания всегда захватывали переменную целиком, без точного пути захвата. Это означает, что для примера, использованного в разделе Типы замыканий, сгенерированный тип замыкания вместо этого выглядел бы примерно так:
struct Closure<'a> {
rect : &'a mut Rectangle,
}
impl<'a> FnOnce<()> for Closure<'a> {
type Output = String;
extern "rust-call" fn call_once(self, args: ()) -> String {
self.rect.left_top.x += 1;
self.rect.right_bottom.x += 1;
format!("{:?}", self.rect.left_top)
}
}
и вызов f работал бы следующим образом:
f(Closure { rect: rect });
Разница в точности захвата
Составные типы, такие как структуры, кортежи и перечисления, всегда захватывались целиком, а не по отдельным полям. Как следствие, может потребоваться заимствовать в локальную переменную, чтобы захватить одно поле:
#![allow(unused)] fn main() { use std::collections::HashSet; struct SetVec { set: HashSet<u32>, vec: Vec<u32> } impl SetVec { fn populate(&mut self) { let vec = &mut self.vec; self.set.iter().for_each(|&n| { vec.push(n); }) } } }
Если бы вместо этого замыкание использовало self.vec напрямую, то оно попыталось бы захватить self по изменяемой ссылке. Но поскольку self.set уже заимствован для итерации, код не скомпилировался бы.
Если используется ключевое слово move, то все захваты происходят перемещением или, для типов Copy, копированием, независимо от того, сработало бы заимствование или нет. Ключевое слово move обычно используется, чтобы позволить замыканию пережить захваченные значения, например, если замыкание возвращается или используется для создания нового потока.
Независимо от того, будут ли данные прочитаны замыканием, т.е. в случае подстановочных образцов, если переменная, определенная вне замыкания, упоминается внутри замыкания, переменная будет захвачена целиком.
Разница в порядке сброса
Поскольку составные типы захватываются целиком, замыкание, которое захватывает один из этих составных типов по значению, сбросит всю захваченную переменную одновременно с тем, как замыкание будет сброшено.
#![allow(unused)] fn main() { { let tuple = (String::from("foo"), String::from("bar")); { let c = || { // --------------------------+ // tuple захвачен в замыкание | drop(tuple.0); // | }; // | } // 'c' и 'tuple' сброшены здесь ------------+ } }