英文:
"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 = &mut foo;
let mut called = false;
let mut c = || {
if !called {
borrower = &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<'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,
}
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
- '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 &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 = &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(&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;
});
});
}
And this code indeed compiles, even with the &mut
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论