“borrowed data escapes outside of closure” 只有在使用 &mut 或线程时才会出现吗?

huangapple go评论85阅读模式
英文:

"borrowed data escapes outside of closure" only when using &mut or threads?

问题

When trying to reassign a reference to point somewhere else from inside a closure, I noticed a strange behavior that I cannot explain, shown by this minimal example:

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

fn main() {
    let mut foo: i32 = 5;
    let mut foo2: i32 = 6;
    let mut borrower = &mut foo; // 在此处和以下没有 mut 编译通过
    let mut c = || {
        borrower = &mut foo2;    // 在此处和以上没有 mut 编译通过
    };
}

this yields the following error ONLY when the references are &mut:

这只在引用为 &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;
   |         ^^^^^^^^^^^^^^^^^^^^

What does this error actually mean here? Why would it be unsafe to do this, given that it's clear that the closure is only alive while foo2 is still alive? Why does it matter whether it's a &mut reference or not?

这个错误在这里实际上意味着什么?为什么这样做会不安全,考虑到明确闭包仅在 foo2 仍然存在时存在?是否是一个 &mut 引用是否重要?

When trying the same from a a scoped thread, it NEVER compiles, with or without mut:

尝试从一个作用域线程中尝试相同的操作时,它永远不会编译,无论是否有 mut:

fn main() {
    let mut foo: i32 = 5;
    let mut foo2: i32 = 6;
    let a = Arc::new(Mutex::new(&mut foo)); // 删除 mut 不会修复它
    println!("{}", a.lock().unwrap());
    
    thread::scope(|s| {
        let aa = a.clone();
        s.spawn(move ||{
            *aa.lock().unwrap() = &mut foo2; // 删除 mut 不会修复它
        });
    });
}

When mut is removed, the program compiles without error. Why is the behavior different here from the first example, where removing mut satisfies the compiler?

当删除 mut 时,程序将无错误编译。为什么这里的行为与第一个示例不同,删除 mut 可以满足编译器?

My research has led me to believe that it might have something to do with the FnOnce, FnMut, and Fn Traits of closures, but I am stuck.

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

英文:

When trying to reassign a reference to point somewhere else from inside a closure, I noticed a strange behavior that I cannot explain, shown by this minimal example:

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

this yields the following error ONLY when the references are &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;
   |         ^^^^^^^^^^^^^^^^^^^^

What does this error actually mean here? Why would it be unsafe to do this, given that it's clear that the closure is only alive while foo2 is still alive? Why does it matter whether it's a &mut reference or not?

When trying the same from a a scoped thread, it NEVER compiles, with or without 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
        });
    });
}

When mut is removed, the program compiles without error.
Why is the behavior different here from the first example, where removing mut satisfies the compiler?

My research has lead me to believe that it might have something to do with the FnOnce, FnMut and Fn Traits of closures, but I am stuck.

答案1

得分: 5

以下是代码的翻译部分:

如果编译器按照您想要的方式查看代码,此代码将有效:我们在当前分支中未借用 foo2,因此它没有被借用。但显然,此代码并非如此:我们在从上一个闭包调用中借用 foo2 时对其进行了更改。

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

闭包被解糖为实现 Fn 系列特质的结构体。这是我们的闭包大致解糖的方式:

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

// 将 `FnOnce` 转发到 `FnMut`。这对我们来说并不是特别重要,我只是为了完整性而保留它。
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;
});

尽管要求 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;
        });
    });
}

这段代码确实编译通过,即使带有 &mut

英文:

Consider the following code:

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

If the compiler would look at the code like you wanted it to, this code would be valid: we don't borrow foo2 in the current branch, so it is not borrowed. But this code is clearly not: we mutate foo2 while it is borrowed, from the previous closure call.

If you'll ask how the compiler figures this out, then we'll need to look at how the desugared closure looks like.

Closures desugars to structs that implement the Fn family of traits. Here's how, roughly, our closure desugars:

struct Closure&lt;&#39;borrower, &#39;foo2&gt; {
    borrower: &amp;&#39;borrower mut &amp;&#39;foo2 mut i32,
    foo2: &amp;&#39;foo2 mut i32,
}

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

impl&lt;&#39;borrower, &#39;foo2&gt; FnMut&lt;()&gt; for Closure&lt;&#39;borrower, &#39;foo2&gt; {
    extern &quot;rust-call&quot; fn call_mut&lt;&#39;this&gt;(&amp;&#39;this mut self, (): ()) -&gt; Self::Output {
        *self.borrower = self.foo2;
    }
}

// let mut c = || {
//     borrower = &amp;mut foo2;
// };
let mut c = Closure {
    borrower: &amp;mut borrower,
    foo2: &amp;mut foo2,
}

See the problem? We're trying to assign self.foo2 to *self.borrower, but we cannot move out of self.foo as we only have a mutable reference to self. We can mutably borrow it, but only for the lifetime of self - &#39;this, and it is not enough. We need the full lifetime foo2.

However, when the references are immutable, we don't need to move out of self.foo2 - we can just copy it. This creates a reference with the desired lifetime, because immutable references are Copy.

The reason it wants in the code I brought in a comment, without Mutex (without move, I hope it is obvious why it doesn't work with move), is that spawn() takes FnOnce, so the compiler knows we cannot call the closure twice. Technically, We have self and not &amp;mut self, so we can move out of its fields.

It works if we force FnOnce too:

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

The reason it does not work with your scoped threads snippet, even though it requires FnOnce, is completely different: it's again because of the move. Because of it, foo2 is local to the closure, and borrowing it yields a reference that is only valid in the closure, as it is destroyed when the closure exits. Fixing it requires borrowing foo2 instead of moving it. We cannot get rid of the move because of aa, so we need to partially move closure captures. The way to do that is as follows:

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

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

And this code indeed compiles, even with the &amp;mut.

huangapple
  • 本文由 发表于 2023年7月18日 10:23:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/76709164.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定