Простое понимание замыканий в Rust

от автора

У вас бывало такое, что вы никак не можете скомпилировать код с замыканиями в Rust? Уже и все варианты Fn-трейтов перебрали, и move написали везде, где можно, а borrow checker все равно не унимается? И тут оказывается, что просто нужно внутри замыкания клонировать переданную переменную окружения! Сложно и непонятно. Дурацкий привереда Rust.

На самом деле довольно просто понять, почему так происходит и на что влияет move, а на что — клонирование. Но отсутствие подобного понимания я наблюдаю не только у начинающих программистов, но и у вполне зрелых. Хуже того, есть статьи, в которых это объясняется неправильно.

Итак, ключ к пониманию — это представление, что замыкание на самом деле реализуется компилятором как структура. Причем захваченные переменные окружения становятся полями структуры, а тело замыкания становится телом метода для вызова (одного из трех возможных: Fn::call, FnMut::call_mut, FnOnce::call_once).

Рассмотрим пример:

fn new_closure(a: i32) -> impl Fn(i32) -> i32 {     move |x| a * x }

Заметьте, мы возвращаем тип, реализующий Fn, однако при этом должны написать move перед определением замыкания. В некоторых статьях ошибочно утверждается, что move необходим для FnOnce-замыканий. Ошибка заключается в том, что move относят не к самому объекту замыкания и способу хранения переменных окружения в нем, а к способу вызова функционального тела замыкания. То есть, move влияет на то, захватит ли само замыкание (не его тело, а его структура!) переменные окружения во владение или будет заимствовать по ссылке.

Пример определения замыкания выше можно упрощенно представить таким псевдокодом:

// Для `move |x| a * x`  struct Closure {     a: i32 }  impl Fn<i32> for Closure {     type Output = i32;      fn call(&self, x: i32) -> F::Output {         self.a * x     } }

// Для `|x| a * x`  struct Closure<'a> {     a: &'a i32 }  impl Fn<i32> for Closure<'_> {     type Output = i32;      fn call(&self, x: i32) -> F::Output {         self.a * x     } }

Как видно, никаких изменений тело замыкания не претерпело. Поэтому и FnOnce-замыкания могут не владеть своим окружением, а заимствовать его:

fn map(x: usize, fun: impl FnOnce(usize) -> usize) -> usize {     fun(x) }  let msg = String::from("hello");  let product = |x| msg.len() * x; // Заимствует `msg`  let b = map(7, product); println!("{msg} {b}");

hello 35

Но такое замыкание нельзя будет вернуть из функции:

fn new_closure(msg: String) -> impl FnOnce(usize) -> usize {     |x| msg.len() * x }

При компиляции возникает ошибка:

error[E0373]: closure may outlive the current function, but it borrows `msg`, which is owned by the current function  --> src/main.rs:2:5   | 2 |     |x| msg.len() * x   |     ^^^ --- `msg` is borrowed here   |     |   |     may outlive borrowed value `msg`   | note: closure is returned here  --> src/main.rs:2:5   | 2 |     |x| msg.len() * x   |     ^^^^^^^^^^^^^^^^^ help: to force the closure to take ownership of `msg` (and any other referenced variables), use the `move` keyword   | 2 |     move |x| msg.len() * x   |     ++++

И понятно почему так происходит. Структура

struct Closure<'a> {     msg: &'a String }

Имеет лайфтайм области жизни внешней переменной msg, которая уничтожается в конце тела функции. Значит само замыкание не может пережить вызов функции и не может быть возвращено из нее. Нужна структура замыкания такого вида:

struct Closure {     msg: String }

Можно этого добиться с помощью слова move, как советует компилятор, но можно сделать и иначе:

fn new_closure(msg: String) -> impl FnOnce(usize) -> usize {     |x| msg.into_bytes().len() * x }

Такой код скомпилируется. Потому что в теле замыкания вызов into_bytes завладевает переменной msg, и компилятор сам догадывается её переместить в замыкание, а не заимствовать.

Рассмотрим ситуацию, когда возникает необходимость клонировать переменную окружения внутри замыкания. С трейтом Fn последний пример не работает:

fn new_closure(msg: String) -> impl Fn(usize) -> usize {     |x| msg.into_bytes().len() * x }

Ошибка:

error[E0507]: cannot move out of `msg`, a captured variable in an `Fn` closure  --> src/main.rs:2:9   | 1 | fn new_closure(msg: String) -> impl Fn(usize) -> usize {   |                --- captured outer variable 2 |     |x| msg.into_bytes().len() * x   |     --- ^^^ ------------ `msg` moved due to this method call   |     |   |   |     |   move occurs because `msg` has type `String`, which does not implement the `Copy` trait   |     captured by this `Fn` closure   | note: this function takes ownership of the receiver `self`, which moves `msg`

Потому что реализовано наше замыкание будет примерно так:

struct Closure {     msg: String }  impl Fn<usize> for Closure {     type Output = usize;      fn call(&self, x: usize) -> F::Output {         self.msg.into_bytes().len() * x     } }

self в функцию вызова принимается по ссылке, поэтому невозможно переместить self.msg внутрь into_bytes. Если только его не склонировать:

fn new_closure(msg: String) -> impl Fn(usize) -> usize {     |x| msg.clone().into_bytes().len() * x }

Однако, опять ошибка! Теперь уже потому, что msg стал заимствоваться внутри тела замыкания, при вызове msg.clone(), и компилятор сохранил его в структуре как ссылочное поле. Здесь мы обязаны написать move руками, чтобы заставить компилятор сделать поле msg в структуре замыкания владеющим:

fn new_closure(msg: String) -> impl Fn(usize) -> usize {     move |x| msg.clone().into_bytes().len() * x }

На заметку: если необходимо переместить некоторые переменные в замыкание, а другие принять по ссылке, то можно сделать так:

let msg = String::from("hello"); let c = 25; let product = { let msg = &msg; move |x| msg.len() * x * c };

Итак, что в итоге?

  • Замыкание — это структура, в поля которой записываются переменные окружения, а тело становится телом метода Fn::call, FnMut::call_mut или FnOnce::call_once.
  • Ключевое слово move управляет способом захвата переменных в сам объект замыкания, а не их использованием в теле замыкания при вызове.
  • Чтобы разрешить конфликты владения в самом теле, иногда приходится использовать клонирование.

Надеюсь теперь с замыканиями будет меньше мороки. Подобный же подход вы можете применить к пониманию того, как работают async-функции и блоки. После этого, разруливание ссылок и перемещений для вас станет делом техники.


ссылка на оригинал статьи https://habr.com/ru/post/699596/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *