为什么我不能从闭包返回对外部变量的可变引用



当我遇到这个有趣的场景时,我正在玩 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并且xCopy,编译器检测到闭包只能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();
}

这里有两件主要的事情在起作用:

  1. 闭包无法返回对其环境的引用
  2. 对可变引用的
  3. 可变引用只能使用外部引用的生存期(与不可变引用不同)

返回对环境引用的闭包

闭包不能返回任何生存期为self(闭包对象)的引用。为什么?每个闭包都可以称为FnOnce,因为这是FnMut的超级特质,而Fn的超级特质又是超级特质。FnOnce有这样的方法:

fn call_once(self, args: Args) -> Self::Output;

请注意,self是按值传递的。因此,由于self被消耗(现在存在于call_once函数中),我们无法返回对它的引用 - 这等同于返回对局部函数变量的引用。

理论上,call_mut将允许返回对self的引用(因为它接收&mut self)。但是由于call_oncecall_mutcall都是用同一个主体实现的,闭包一般不能返回对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

但是为什么编译器不抛出此错误呢?

简短的回答是编译器首先执行类型检查,然后借用检查。

长答案

封盖由两部分组成:

闭包
  • 的状态:包含闭包捕获的所有变量的结构

  • 闭包的逻辑FnOnceFnMutFn特征的实现

在这种情况下,闭包的状态是可变引用y,逻辑是仅返回可变引用的闭包{ &mut y }的主体。

当遇到引用时,Rust 控制两个方面:

  1. 状态:如果引用指向有效的内存切片(即生命周期有效期的只读部分);

  2. 逻辑:如果内存切片是锯齿的,换句话说,如果它同时指向多个引用;

请注意,为了避免内存别名,禁止从借用的内容中移出。

Rust 编译器通过几个阶段执行其工作,这是一个简化的工作流程:

.rs input -> AST -> HIR -> HIR postprocessing -> MIR -> HIR postprocessing -> LLVM IR -> binary

编译器报告生存期问题,因为它首先在HIR postprocessing中执行类型检查阶段(包括生存期分析),然后,如果成功,则在MIR postprocessing阶段执行借用检查。

最新更新