временная стоимость упала при заимствовании с закрытием захвата

Рассмотрим следующий пример (детская площадка):

struct Animal<'a> {
    format: &'a dyn Fn() -> (),
}

impl <'a>Animal<'a> {
    pub fn set_formatter(&mut self, _fmt: &'a dyn Fn() -> ()) -> () {} // Getting rid of 'a here satisfies the compiler
    pub fn bark(&self) {}
}

fn main() {
    let mut dog: Animal = Animal { format: &|| {()} };
    let x = 0;
    dog.set_formatter(&|| {
        println!("{}", x); // Commenting this out gets rid of the error. Why?
    });
    dog.bark(); // Commenting this out gets rid of the error. Why?
}

Это дает следующую ошибку компиляции:

Compiling playground v0.0.1 (/playground)
error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:13:24
   |
13 |       dog.set_formatter(&|| {
   |  ________________________^
14 | |         println!("{}", x); // Commenting this out gets rid of the error. Why?
15 | |     });
   | |     ^ - temporary value is freed at the end of this statement
   | |_____|
   |       creates a temporary which is freed while still in use
16 |       dog.bark(); // Commenting this out gets rid of the error. Why?
   |       --- borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value

error: aborting due to previous error

For more information about this error, try `rustc --explain E0716`.
error: could not compile `playground`

To learn more, run the command again with --verbose.

Это имеет смысл, поскольку закрытие, которое я передаю dog.set_formatter(...), действительно является временным, которое (я полагаю) освобождается, когда выполнение переходит к dog.bark();.

Я знаю, что избавление от явной аннотации времени жизни в реализации set_formatter, похоже, удовлетворяет компилятор (обратите внимание на отсутствующий 'a перед dyn):

pub fn set_formatter(&mut self, _fmt: & dyn Fn() -> ()) -> () {}

Однако я не понимаю следующего:

  1. Почему проблема исчезает, если я закомментировал println!("{}", x); внутри закрытия? Я все еще передаю временное сообщение, на которое я ожидаю, что компилятор будет жаловаться, но это не так.
  2. Почему проблема исчезает, если я комментирую dog.bark(); в конце? Опять же, я все еще прохожу временное закрытие, которое освобождено, но теперь компилятор доволен. Почему?

person Attila Kun    schedule 31.01.2021    source источник


Ответы (1)


Первое, что нужно понять, это то, что &|| () имеет время жизни 'static:

fn main() {
    let closure: &'static dyn Fn() = &|| (); // compiles
}

Еще одна вещь, о которой стоит упомянуть, заключается в том, что время жизни закрытия не может превышать время жизни любой из переменных, которые оно захватывает из своей среды, поэтому, если мы попытаемся передать нестатическую переменную нашему статическому закрытию, она не сможет скомпилировать:

fn main() {
    let x = 0; // non-static temporary variable
    let closure: &'static dyn Fn() = &|| {
        println!("{}", x); // x reference captured by closure
    }; // error: trying to capture non-static variable in static closure
}

Мы вернемся к этому. В любом случае, если у меня есть структура, которая является универсальной по ссылкам, и я передаю ей ссылку 'static, у меня будет 'static экземпляр этой структуры:

struct Dog<'a> {
    format: &'a dyn Fn(),
}

fn main() {
    let dog: Dog<'static> = Dog { format: &|| () }; // compiles
}

Во-вторых, необходимо понять, что как только вы создадите экземпляр типа, вы не сможете его изменить. Сюда входят любые его общие параметры, включая время жизни. Если у вас есть Dog<'static>, он всегда будет Dog<'static>, вы не можете преобразовать его в Dog<'1> для некоторого анонимного времени жизни '1, которое короче, чем 'static.

Это имеет серьезные последствия, одно из которых состоит в том, что ваш set_formatter метод, вероятно, ведет себя не так, как вы думаете. Если у вас есть Dog<'static>, вы можете только передать 'static форматеры в set_formatter. Метод выглядит так:

impl<'a> Dog<'a> {
    fn set_formatter(&mut self, _fmt: &'a dyn Fn()) {}
}

Но поскольку мы знаем, что работаем с Dog<'static>, мы можем заменить общий параметр времени жизни 'a на 'static, чтобы увидеть, с чем мы действительно работаем:

// what the impl would be for Dog<'static>
impl Dog {
    fn set_formatter(&mut self, _fmt: &'static dyn Fn()) {}
}

Итак, теперь, когда мы разобрались со всем этим контекстом, давайте перейдем к вашим актуальным вопросам.

Почему проблема исчезает, если я закомментировал println!("{}", x); внутри закрытия? Я все еще передаю временное сообщение, на которое я ожидаю, что компилятор будет жаловаться, но это не так.

Почему это не удается, с комментариями:

struct Dog<'a> {
    format: &'a dyn Fn(),
}

impl<'a> Dog<'a> {
    fn set_formatter(&mut self, _fmt: &'a dyn Fn()) {}
}

fn main() {
    let mut dog: Dog<'static> = Dog { format: &|| () };
    
    // x is a temporary value on the stack with a non-'static lifetime
    let x = 0;
    
    // this closure captures x by reference which ALSO gives it a non-'static lifetime
    // and you CANNOT pass a non-'static closure to a Dog<'static>
    dog.set_formatter(&|| {
        println!("{}", x); 
    });
}

Причина, по которой эта ошибка исправляется путем комментирования строки println!("{}", x);, заключается в том, что она снова дает замыканию время жизни 'static, поскольку оно больше не заимствует переменную, отличную от 'static, x.

Почему проблема исчезает, если я закомментировал dog.bark(); в конце? Опять же, я все еще прохожу временное закрытие, которое освобождено, но теперь компилятор доволен. Почему?

Этот странный крайний случай, кажется, возникает только тогда, когда мы явно не аннотируем переменную dog с помощью Dog<'static>. Когда переменная не имеет явной аннотации типа, компилятор пытается вывести ее тип, но делает это лениво и пытается быть как можно более гибким, давая программисту преимущество сомнения, чтобы сделать код компилировать. Он действительно должен выдать ошибку компиляции даже без dog.bark(), но этого не происходит по каким-то загадочным причинам. Дело в том, что это не строка dog.bark(), из-за которой код не компилируется, он должен не компилироваться в строке set_formatter независимо от того, но по какой-либо причине компилятор не потрудится выдать ошибку, пока вы не попытаетесь снова использовать dog после этой оскорбительной строки. Даже простое падение dog вызовет ошибку:

struct Dog<'a> {
    format: &'a dyn Fn(),
}

impl<'a> Dog<'a> {
    fn set_formatter(&mut self, _fmt: &'a dyn Fn()) {}
}

fn main() {
    let mut dog = Dog { format: &|| () };
    let x = 0;
    
    dog.set_formatter(&|| {
        println!("{}", x); 
    });
    
    drop(dog); // triggers "temp freed" error above
}

И раз уж мы зашли так далеко, давайте ответим на ваш неофициальный третий вопрос, перефразированный мной:

Почему избавление от 'a в методе set_formatter удовлетворяет компилятор?

Потому что это фактически меняет то, что для Dog<'static>:

// what the impl would be for Dog<'static>
impl Dog {
    fn set_formatter(&mut self, _fmt: &'static dyn Fn()) {}
}

В это:

// what the impl would now be for Dog<'static>
impl Dog {
    fn set_formatter(&mut self, _fmt: &dyn Fn()) {}
}

Итак, теперь вы можете передавать закрытие, отличное от 'static, в Dog<'static>, хотя это бессмысленно, поскольку метод на самом деле ничего не делает, и компилятор снова будет жаловаться, когда вы действительно попытаетесь установить закрытие в структуре Dog<'static>.

person pretzelhammer    schedule 31.01.2021