只有在使用&mut或线程时,"borrowed data escapes outside of closure"才会发生。

4
当我尝试在闭包内部重新分配引用指向其他位置时,我注意到了一个奇怪的行为,这个最简示例展示了这种行为,但我无法解释。
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
    };
}

当引用为&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;
   |         ^^^^^^^^^^^^^^^^^^^^

这个错误在这里实际上意味着什么?鉴于闭包只在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
        });
    });
}

当删除mut时,程序可以编译通过,没有错误。 为什么这里的行为与第一个例子不同,而在第一个例子中删除mut可以满足编译器?
我的研究让我相信这可能与闭包的FnOnce、FnMut和Fn特性有关,但是我卡住了。

这是一个更简单的作用域线程示例,没有使用Mutex点击此处查看示例 - Chayim Friedman
这是一个更简单的作用域线程示例,没有使用Mutex点击此处查看示例 - Chayim Friedman
这是一个更简单的作用域线程示例,没有使用Mutex点击此处查看示例 - undefined
如果你在例子中移除move关键字,它会正常工作,但奇怪的是,即使加上了&mut,它仍然能够工作,与第一个例子相反。很奇怪。 - JMC
如果在你的示例中去掉move关键字,它就能正常工作,但奇怪的是,即使有&mut,它也能正常工作,与第一个示例相反。奇怪。 - JMC
1个回答

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;
}

如果编译器能按照你的意愿来查看代码,那么这段代码就是有效的:在当前分支中,我们没有借用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,
}

看到问题了吗?我们试图将`self.foo2`赋值给`*self.borrower`,但是我们不能从`self.foo`中移出它,因为我们只有对`self`的可变引用。我们可以进行可变借用,但只能在`self`的生命周期内-`'this`,这是不够的。我们需要完整的生命周期`foo2`。
然而,当引用是不可变的时候,我们不需要移出`self.foo2`-我们可以直接复制它。这样就创建了一个具有所需生命周期的引用,因为不可变引用是可复制的。
原因是它想要在我带入的代码中加入一个注释,没有互斥锁(没有移动,希望很明显为什么不能使用移动),这是因为spawn()需要FnOnce,所以编译器知道我们不能调用闭包两次。从技术上讲,我们有self而不是&mut self,所以我们可以移出其字段。
如果我们也强制使用FnOnce,它就能工作了。
fn force_fnonce(f: impl FnOnce()) {}
force_fnonce(|| {
    borrower = &mut foo2;
});

即使您的作用域线程片段需要FnOnce,但它无法与您的作用域线程片段一起工作的原因完全不同:这是因为move。由于move,foo2仅在闭包中有效,并且借用它会产生一个只在闭包中有效的引用,因为当闭包退出时,它将被销毁。修复它需要借用foo2而不是移动它。由于aa的存在,我们无法摆脱move,因此我们需要部分移动闭包捕获。实现这一点的方法如下:
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;
        });
    });
}

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

非常感谢这个很好的解释。然而,在这种情况下,我觉得编译器的错误消息不太清晰,因为它说“借用的数据逃逸了闭包之外”,而实际上更像是“无法将可变引用移出闭包”的情况。 - JMC
非常感谢这个很好的解释。然而,在这种情况下,我觉得编译器的错误信息并不是很清楚,因为它说“借用的数据逃逸到闭包外”,而实际上更像是“无法将可变引用移出闭包”。 - JMC
非常感谢这个很棒的解释。然而,在这种情况下,我觉得编译器的错误信息并不是很清楚,因为它说的是“借用的数据逃逸到闭包外”,而实际上更像是“无法将可变引用移出闭包”。 - undefined
@JMC 不是移动,而是尝试重新借用它,就像 &amp;mut *self.foo2 这样。但这会导致引用的生命周期变短,所以会出现“借用的数据逃逸到闭包外部”的情况。 - Chayim Friedman
@JMC 而不是移动,它尝试重新借用它,就像 &mut *self.foo2。但这会导致引用的生命周期变短,因此会出现“借用的数据逃逸到闭包外部”的情况。 - Chayim Friedman
@JMC 而不是移动,它尝试重新借用它,就像&mut *self.foo2一样。但这会导致引用的生命周期变短,因此会出现"借用的数据逃逸到闭包外部"的情况。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接