为什么我不能 return 闭包中对外部变量的可变引用?
Why can I not return a mutable reference to an outer variable from a closure?
当我遇到这个有趣的场景时,我正在玩 Rust 闭包:
fn main() {
let mut y = 10;
let f = || &mut y;
f();
}
这给出了一个错误:
error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
--> src/main.rs:4:16
|
4 | let f = || &mut y;
| ^^^^^^
|
note: first, the lifetime cannot outlive the lifetime as defined on the body at 4:13...
--> src/main.rs:4:13
|
4 | let f = || &mut y;
| ^^^^^^^^^
note: ...so that closure can access `y`
--> src/main.rs:4:16
|
4 | let f = || &mut y;
| ^^^^^^
note: but, the lifetime must be valid for the call at 6:5...
--> src/main.rs:6:5
|
6 | f();
| ^^^
note: ...so type `&mut i32` of expression is valid during the expression
--> src/main.rs:6:5
|
6 | f();
| ^^^
尽管编译器试图逐行解释它,但我仍然不明白它到底在抱怨什么。
它是想说可变引用不能比封闭的闭包长寿吗?
如果我删除调用 f()
,编译器不会抱怨 f()
。
考虑这段代码:
fn main() {
let mut y: u32 = 10;
let ry = &mut y;
let f = || ry;
f();
}
之所以有效,是因为编译器能够推断出 ry
的生命周期:引用 ry
存在于 y
.
的相同范围内
现在,您的代码的等效版本:
fn main() {
let mut y: u32 = 10;
let f = || {
let ry = &mut y;
ry
};
f();
}
现在编译器分配给 ry
与闭包主体范围关联的生命周期,而不是与主体关联的生命周期。
另请注意,不可变参考案例有效:
fn main() {
let mut y: u32 = 10;
let f = || {
let ry = &y;
ry
};
f();
}
这是因为 &T
具有复制语义,而 &mut T
具有移动语义,请参阅 了解更多详情。
缺失的一块
编译器抛出与生命周期相关的错误:
cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
但正如 Sven Marnach 所指出的,还有一个与错误相关的问题
cannot move out of borrowed content
但是为什么编译器不抛出这个错误呢?
简短的回答是编译器首先执行类型检查,然后执行借用检查。
长答案
一个闭包由两部分组成:
闭包的状态:包含闭包捕获的所有变量的结构
闭包的逻辑:FnOnce
、FnMut
或Fn
特征[=的实现31=]
在这种情况下,闭包的状态是可变引用 y
,逻辑是闭包的主体 { &mut y }
,它只是 returns 一个可变引用。
当遇到引用时,Rust 控制两个方面:
状态:如果引用指向一个有效的内存片,(即只读部分的生存期有效性);
逻辑:如果内存片是别名的,换句话说,如果它同时被多个引用指向;
请注意,为了避免内存别名,禁止从借用的内容中移出。
Rust 编译器通过 several stages 执行其工作,这是一个简化的工作流程:
.rs input -> AST -> HIR -> HIR postprocessing -> MIR -> HIR postprocessing -> LLVM IR -> binary
编译器报生命周期问题,因为它首先在HIR postprocessing
执行类型检查阶段(包括生命周期分析),然后如果成功,在MIR postprocessing
阶段执行借用检查。
这里主要有两点:
- 闭包不能 return 引用它们的环境
- 对可变引用的可变引用只能使用外部引用的生命周期(与不可变引用不同)
闭包return对环境的引用
闭包不能return任何生命周期为self
(闭包对象)的引用。这是为什么?每个闭包都可以称为 FnOnce
,因为这是 FnMut
的超特征,而后者又是 Fn
的超特征。 FnOnce
有这个方法:
fn call_once(self, args: Args) -> Self::Output;
注意self
是按值传递的。因此,由于 self
被消耗(并且现在存在于 call_once
函数中)我们不能 return 引用它——这等同于 returning 对本地的引用函数变量。
理论上,call_mut
将允许 return 引用 self
(因为它收到 &mut self
)。但是由于 call_once
、call_mut
和 call
都是用同一个主体实现的,闭包一般不能 return 引用 self
(即:他们捕获的环境)。
可以肯定的是:闭包可以捕获引用和 return 那些!他们可以通过 引用 和 return 该引用来捕获。那些东西是不同的东西。它只是关于存储在闭包类型中的内容。如果类型中存储了引用,则可以对其进行 returned。但是我们不能 return 引用存储在闭包类型中的任何东西。
嵌套可变引用
考虑这个函数(注意参数类型暗示 'inner: 'outer
;'outer
比 'inner
短):
fn foo<'outer, 'inner>(x: &'outer mut &'inner mut i32) -> &'inner mut i32 {
*x
}
这不会编译。乍一看,它似乎应该可以编译,因为我们只是剥离了一层引用。它确实适用于不可变引用!但是这里的可变引用是不同的,以保持稳健性。
不过 return &'outer mut i32
没关系。但是不可能得到更长(内部)寿命的直接参考。
手动编写闭包
让我们尝试手动编写您尝试编写的闭包代码:
let mut y = 10;
struct Foo<'a>(&'a mut i32);
impl<'a> Foo<'a> {
fn call<'s>(&'s mut self) -> &'??? mut i32 { self.0 }
}
let mut f = Foo(&mut y);
f.call();
returned 引用应该有多少生命周期?
- 不可能是
'a
,因为我们基本都有一个&'s mut &'a mut i32
。并且如上所述,在这种嵌套可变引用情况下,我们无法提取更长的生命周期!
- 但它也不能是
's
,因为这意味着闭包 return 的生命周期为 'self
("borrowed from self
")。如上所述,闭包不能做到这一点。
所以编译器无法为我们生成闭包实现。
简短版
闭包 f
存储对 y
的可变引用。如果它被允许 return 这个引用的副本,你最终会得到两个同时对 y
的可变引用(一个在闭包中,一个 returned),这是被禁止的Rust 的内存安全规则。
长版
闭包可以认为是
struct __Closure<'a> {
y: &'a mut i32,
}
因为它包含一个可变引用,闭包被称为FnMut
,本质上与定义
fn call_mut(&mut self, args: ()) -> &'a mut i32 { self.y }
由于我们只有对闭包本身的可变引用,我们无法将字段 y
移出,也无法复制它,因为可变引用不是 Copy
.
我们可以通过强制将闭包调用为 FnOnce
而不是 FnMut
来欺骗编译器接受代码。此代码工作正常:
fn main() {
let x = String::new();
let mut y: u32 = 10;
let f = || {
drop(x);
&mut y
};
f();
}
由于我们在闭包范围内使用 x
而 x
不是 Copy
,编译器检测到闭包只能是 FnOnce
。调用 FnOnce
闭包通过值传递闭包本身,因此我们可以将可变引用移出。
强制闭包为 FnOnce
的另一种更明确的方法是将其传递给具有特征绑定的通用函数。此代码也可以正常工作:
fn make_fn_once<'a, T, F: FnOnce() -> T>(f: F) -> F {
f
}
fn main() {
let mut y: u32 = 10;
let f = make_fn_once(|| {
&mut y
});
f();
}
当我遇到这个有趣的场景时,我正在玩 Rust 闭包:
fn main() {
let mut y = 10;
let f = || &mut y;
f();
}
这给出了一个错误:
error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
--> src/main.rs:4:16
|
4 | let f = || &mut y;
| ^^^^^^
|
note: first, the lifetime cannot outlive the lifetime as defined on the body at 4:13...
--> src/main.rs:4:13
|
4 | let f = || &mut y;
| ^^^^^^^^^
note: ...so that closure can access `y`
--> src/main.rs:4:16
|
4 | let f = || &mut y;
| ^^^^^^
note: but, the lifetime must be valid for the call at 6:5...
--> src/main.rs:6:5
|
6 | f();
| ^^^
note: ...so type `&mut i32` of expression is valid during the expression
--> src/main.rs:6:5
|
6 | f();
| ^^^
尽管编译器试图逐行解释它,但我仍然不明白它到底在抱怨什么。
它是想说可变引用不能比封闭的闭包长寿吗?
如果我删除调用 f()
,编译器不会抱怨 f()
。
考虑这段代码:
fn main() {
let mut y: u32 = 10;
let ry = &mut y;
let f = || ry;
f();
}
之所以有效,是因为编译器能够推断出 ry
的生命周期:引用 ry
存在于 y
.
现在,您的代码的等效版本:
fn main() {
let mut y: u32 = 10;
let f = || {
let ry = &mut y;
ry
};
f();
}
现在编译器分配给 ry
与闭包主体范围关联的生命周期,而不是与主体关联的生命周期。
另请注意,不可变参考案例有效:
fn main() {
let mut y: u32 = 10;
let f = || {
let ry = &y;
ry
};
f();
}
这是因为 &T
具有复制语义,而 &mut T
具有移动语义,请参阅
缺失的一块
编译器抛出与生命周期相关的错误:
cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
但正如 Sven Marnach 所指出的,还有一个与错误相关的问题
cannot move out of borrowed content
但是为什么编译器不抛出这个错误呢?
简短的回答是编译器首先执行类型检查,然后执行借用检查。
长答案
一个闭包由两部分组成:
闭包的状态:包含闭包捕获的所有变量的结构
闭包的逻辑:
FnOnce
、FnMut
或Fn
特征[=的实现31=]
在这种情况下,闭包的状态是可变引用 y
,逻辑是闭包的主体 { &mut y }
,它只是 returns 一个可变引用。
当遇到引用时,Rust 控制两个方面:
状态:如果引用指向一个有效的内存片,(即只读部分的生存期有效性);
逻辑:如果内存片是别名的,换句话说,如果它同时被多个引用指向;
请注意,为了避免内存别名,禁止从借用的内容中移出。
Rust 编译器通过 several stages 执行其工作,这是一个简化的工作流程:
.rs input -> AST -> HIR -> HIR postprocessing -> MIR -> HIR postprocessing -> LLVM IR -> binary
编译器报生命周期问题,因为它首先在HIR postprocessing
执行类型检查阶段(包括生命周期分析),然后如果成功,在MIR postprocessing
阶段执行借用检查。
这里主要有两点:
- 闭包不能 return 引用它们的环境
- 对可变引用的可变引用只能使用外部引用的生命周期(与不可变引用不同)
闭包return对环境的引用
闭包不能return任何生命周期为self
(闭包对象)的引用。这是为什么?每个闭包都可以称为 FnOnce
,因为这是 FnMut
的超特征,而后者又是 Fn
的超特征。 FnOnce
有这个方法:
fn call_once(self, args: Args) -> Self::Output;
注意self
是按值传递的。因此,由于 self
被消耗(并且现在存在于 call_once
函数中)我们不能 return 引用它——这等同于 returning 对本地的引用函数变量。
理论上,call_mut
将允许 return 引用 self
(因为它收到 &mut self
)。但是由于 call_once
、call_mut
和 call
都是用同一个主体实现的,闭包一般不能 return 引用 self
(即:他们捕获的环境)。
可以肯定的是:闭包可以捕获引用和 return 那些!他们可以通过 引用 和 return 该引用来捕获。那些东西是不同的东西。它只是关于存储在闭包类型中的内容。如果类型中存储了引用,则可以对其进行 returned。但是我们不能 return 引用存储在闭包类型中的任何东西。
嵌套可变引用
考虑这个函数(注意参数类型暗示 'inner: 'outer
;'outer
比 'inner
短):
fn foo<'outer, 'inner>(x: &'outer mut &'inner mut i32) -> &'inner mut i32 {
*x
}
这不会编译。乍一看,它似乎应该可以编译,因为我们只是剥离了一层引用。它确实适用于不可变引用!但是这里的可变引用是不同的,以保持稳健性。
不过 return &'outer mut i32
没关系。但是不可能得到更长(内部)寿命的直接参考。
手动编写闭包
让我们尝试手动编写您尝试编写的闭包代码:
let mut y = 10;
struct Foo<'a>(&'a mut i32);
impl<'a> Foo<'a> {
fn call<'s>(&'s mut self) -> &'??? mut i32 { self.0 }
}
let mut f = Foo(&mut y);
f.call();
returned 引用应该有多少生命周期?
- 不可能是
'a
,因为我们基本都有一个&'s mut &'a mut i32
。并且如上所述,在这种嵌套可变引用情况下,我们无法提取更长的生命周期! - 但它也不能是
's
,因为这意味着闭包 return 的生命周期为'self
("borrowed fromself
")。如上所述,闭包不能做到这一点。
所以编译器无法为我们生成闭包实现。
简短版
闭包 f
存储对 y
的可变引用。如果它被允许 return 这个引用的副本,你最终会得到两个同时对 y
的可变引用(一个在闭包中,一个 returned),这是被禁止的Rust 的内存安全规则。
长版
闭包可以认为是
struct __Closure<'a> {
y: &'a mut i32,
}
因为它包含一个可变引用,闭包被称为FnMut
,本质上与定义
fn call_mut(&mut self, args: ()) -> &'a mut i32 { self.y }
由于我们只有对闭包本身的可变引用,我们无法将字段 y
移出,也无法复制它,因为可变引用不是 Copy
.
我们可以通过强制将闭包调用为 FnOnce
而不是 FnMut
来欺骗编译器接受代码。此代码工作正常:
fn main() {
let x = String::new();
let mut y: u32 = 10;
let f = || {
drop(x);
&mut y
};
f();
}
由于我们在闭包范围内使用 x
而 x
不是 Copy
,编译器检测到闭包只能是 FnOnce
。调用 FnOnce
闭包通过值传递闭包本身,因此我们可以将可变引用移出。
强制闭包为 FnOnce
的另一种更明确的方法是将其传递给具有特征绑定的通用函数。此代码也可以正常工作:
fn make_fn_once<'a, T, F: FnOnce() -> T>(f: F) -> F {
f
}
fn main() {
let mut y: u32 = 10;
let f = make_fn_once(|| {
&mut y
});
f();
}