当我遇到这个有趣的场景时,我正在玩 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
存储对y
的可变引用。 如果允许它返回此引用的副本,您最终将同时得到两个对y
的可变引用(一个在闭包中,一个返回),这是 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
闭包的另一种更明确的方法是将其传递给具有 trait 绑定的泛型函数。 这段代码也可以正常工作:
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();
}
这里有两件主要的事情在起作用:
- 闭包无法返回对其环境的引用 对可变引用的
- 可变引用只能使用外部引用的生存期(与不可变引用不同)
返回对环境引用的闭包
闭包不能返回任何生存期为self
(闭包对象)的引用。为什么?每个闭包都可以称为FnOnce
,因为这是FnMut
的超级特质,而Fn
的超级特质又是超级特质。FnOnce
有这样的方法:
fn call_once(self, args: Args) -> Self::Output;
请注意,self
是按值传递的。因此,由于self
被消耗(现在存在于call_once
函数中),我们无法返回对它的引用 - 这等同于返回对局部函数变量的引用。
理论上,call_mut
将允许返回对self
的引用(因为它接收&mut self
)。但是由于call_once
、call_mut
和call
都是用同一个主体实现的,闭包一般不能返回对self
的引用(即:对它们捕获的环境的引用)。
可以肯定的是:闭包可以捕获引用并返回这些引用!它们可以通过引用捕获并返回该引用。这些事情是不同的。这只是关于闭包类型中存储的内容。如果类型中存储了引用,则可以返回该引用。但是我们不能返回对闭包类型中存储的任何内容的引用。
嵌套可变引用
考虑这个函数(请注意,参数类型意味着'inner: 'outer
;'outer
短于'inner
):
fn foo<'outer, 'inner>(x: &'outer mut &'inner mut i32) -> &'inner mut i32 {
*x
}
这不会编译。乍一看,它似乎应该编译,因为我们只是剥离了一层引用。它确实适用于不可变的引用!但是可变引用在这里是不同的,以保持健全性。
不过,返回&'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();
返回的引用应具有什么生存期?
- 不可能是
'a
,因为我们基本上有&'s mut &'a mut i32
。如上所述,在这种嵌套的可变引用情况下,我们无法提取更长的生命周期! - 但它也不能
's
,因为这意味着闭包会返回具有'self
生命周期的东西("从self
借用")。如上所述,闭包不能做到这一点。
因此,编译器无法为我们生成闭包 impls。
请考虑以下代码:
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
具有移动语义,有关详细信息,请参阅 &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
特征的实现
在这种情况下,闭包的状态是可变引用y
,逻辑是仅返回可变引用的闭包{ &mut y }
的主体。
当遇到引用时,Rust 控制两个方面:
状态:如果引用指向有效的内存切片(即生命周期有效期的只读部分);
逻辑:如果内存切片是锯齿的,换句话说,如果它同时指向多个引用;
请注意,为了避免内存别名,禁止从借用的内容中移出。
Rust 编译器通过几个阶段执行其工作,这是一个简化的工作流程:
.rs input -> AST -> HIR -> HIR postprocessing -> MIR -> HIR postprocessing -> LLVM IR -> binary
编译器报告生存期问题,因为它首先在HIR postprocessing
中执行类型检查阶段(包括生存期分析),然后,如果成功,则在MIR postprocessing
阶段执行借用检查。