rust 只有在使用&mut或threads时,“借用的数据才会转义到闭包之外”,

hivapdat  于 2023-08-05  发布在  其他
关注(0)|答案(1)|浏览(79)

当试图从闭包内部重新分配一个引用到其他地方时,我注意到一个奇怪的行为,我无法解释,通过这个最小的例子来展示:

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引用很重要?
当从a作用域线程尝试相同的操作时,无论是否使用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特性有关,但我被卡住了。

velaa5lx

velaa5lx1#

请考虑下列程式码:

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 trait家族的结构体的去糖。以下是我们的闭包如何大致去糖:

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--我们可以只 * 复制 * 它。这将创建一个具有所需生存期的引用,因为不可变引用是Copy
我在代码中引入了一个注解,没有Mutex(没有move,我希望很明显为什么它不能与move一起工作),原因是spawn()需要FnOnce,所以编译器知道我们不能调用闭包两次。从技术上讲,我们有self而不是&mut self,因此我们可以移出它的字段。
如果我们强制FnOnce也可以工作:

fn force_fnonce(f: impl FnOnce()) {}
force_fnonce(|| {
    borrower = &mut foo2;
});


它不能与你的scoped threads代码段一起工作的原因是完全不同的,即使它需要FnOnce:又是因为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,这段代码也确实可以编译。

相关问题