仅在使用 &mut 或线程时“借用的数据在闭包之外转义”?

JMC*_*JMC 4 closures rust borrow-checker

当尝试将引用重新分配到闭包内部其他位置时,我注意到一个我无法解释的奇怪行为,如以下最小示例所示:

fn main() {
    let mut foo: i32 = 5;
    let mut foo2: i32 = 6;
    let mut borrower = &mut foo; // compiles OK without mut here and below
    let mut c = || {
        borrower = &mut foo2;    // compiles OK without mut here and above
    };
}
Run Code Online (Sandbox Code Playgroud)

仅当引用为:时才会产生以下错误&mut

error[E0521]: borrowed data escapes outside of closure
  --> src/main.rs:25:9
   |
23 |     let mut borrower = &mut foo;
   |         ------------ `borrower` declared here, outside of the closure body
24 |     let mut c = || {
25 |         borrower = &mut foo2;
   |         ^^^^^^^^^^^^^^^^^^^^
Run Code Online (Sandbox Code Playgroud)

这个错误在这里到底意味着什么?既然很明显闭包只有在foo2还活着的时候才活着,为什么这样做是不安全的呢?为什么参考与否很重要&mut

当从作用域线程中尝试相同的操作时,它永远不会编译,无论有没有mut

fn main() {
    let mut foo: i32 = 5;
    let mut foo2: i32 = 6;
    let a = Arc::new(Mutex::new(&mut foo)); // removing mut does NOT fix it
    println!("{}", a.lock().unwrap());
    
    thread::scope(|s| {
        let aa = a.clone();
        s.spawn(move ||{
            *aa.lock().unwrap() = &mut foo2; // removing mut does NOT fix it
        });
    });
}
Run Code Online (Sandbox Code Playgroud)

删除后mut,程序编译不会出错。为什么这里的行为与第一个示例不同,第一个示例的删除mut可以满足编译器的要求?

我的研究让我相信这可能与闭包的 FnOnce、FnMut 和 Fn Traits 有关,但我被困住了。

Cha*_*man 5

考虑以下代码:

fn main() {
    let mut foo: i32 = 5;
    let mut foo2: i32 = 6;
    let mut borrower = &mut foo;
    let mut called = false;
    let mut c = || {
        if !called {
            borrower = &mut foo2;
            called = true;
        } else {
            foo2 = 123;
        }
    };
    c();
    c();
    *borrower = 456;
}
Run Code Online (Sandbox Code Playgroud)

如果编译器按照您想要的方式查看代码,则此代码将是有效的:我们不会foo2在当前分支中借用,因此它不会被借用。但这段代码显然不是:我们在借用之前的闭包foo2调用时进行了变异。

如果您问编译器如何解决这个问题,那么我们需要看看脱糖的闭包是什么样子的。

闭包脱糖为实现Fn特征系列的结构。我们的闭包脱糖的大致方式如下:

struct Closure<'borrower, 'foo2> {
    borrower: &'borrower mut &'foo2 mut i32,
    foo2: &'foo2 mut i32,
}

// Forward `FnOnce` to `FnMut`. This is not really relevant for us, and I left it only for completeness.
impl FnOnce<()> for Closure<'_, '_> {
    type Output = ();
    extern "rust-call" fn call_once(mut self, (): ()) -> Self::Output {
        self.call_mut(())
    }
}

impl<'borrower, 'foo2> FnMut<()> for Closure<'borrower, 'foo2> {
    extern "rust-call" fn call_mut<'this>(&'this mut self, (): ()) -> Self::Output {
        *self.borrower = self.foo2;
    }
}

// let mut c = || {
//     borrower = &mut foo2;
// };
let mut c = Closure {
    borrower: &mut borrower,
    foo2: &mut foo2,
}
Run Code Online (Sandbox Code Playgroud)

看到问题了吗?我们正在尝试分配self.foo2*self.borrower,但我们无法移出,self.foo因为我们只有对 的可变引用selfself我们可以可变地借用它,但只能在-的生命周期内借用'this,但这还不够。我们需要完整的一生foo2

但是,当引用不可变时,我们不需要移出self.foo2- 我们只需复制它即可。这将创建一个具有所需生命周期的引用,因为不可变引用是Copy.

它想要在我带注释的代码中,没有Mutex(没有move,我希望很明显为什么它不能与 一起工作)的原因move是,所以编译器知道我们不能两次调用闭包。从技术上讲,我们有,也没有,所以我们可以移出它的领域。spawn()FnOnceself&mut self

如果我们FnOnce也强制的话,它会起作用:

fn force_fnonce(f: impl FnOnce()) {}
force_fnonce(|| {
    borrower = &mut foo2;
});
Run Code Online (Sandbox Code Playgroud)

它不适用于您的作用域线程片段,即使它需要FnOnce,也是完全不同的:这又是因为move. 因此,foo2对于闭包而言,它是本地的,借用它会产生一个仅在闭包中有效的引用,因为当闭包退出时它会被销毁。修复它需要借用foo2而不是移动它。我们无法摆脱move因为aa,所以我们需要部分移动闭包捕获。方法如下:

fn main() {
    let mut foo: i32 = 5;
    let mut foo2: i32 = 6;
    let a = Arc::new(Mutex::new(&mut foo));
    println!("{}", a.lock().unwrap());

    thread::scope(|s| {
        let aa = a.clone();
        let foo2_ref = &mut foo2;
        s.spawn(move || {
            *aa.lock().unwrap() = foo2_ref;
        });
    });
}
Run Code Online (Sandbox Code Playgroud)

这段代码确实可以编译,即使使用&mut.