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

时间:2018-10-11 04:30:32

标签: reference rust closures lifetime mutable

遇到这种有趣的情况时,我正在玩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(),编译器不会抱怨。

3 个答案:

答案 0 :(得分:7)

简短版

闭包f存储对y的可变引用。如果允许它返回此引用的副本,则最终将同时对y进行两个可变的引用(一个在闭包中,一个返回),Rust的内存安全规则禁止该引用。

长版

闭包可以认为是

struct __Closure<'a> {
    y: &'a mut i32,
}

由于它包含一个可变的引用,因此闭包称为FnMut,本质上带有定义

fn call_mut(&mut self, args: ()) -> &'a mut i32 { self.y }

由于我们仅对闭包本身具有可变引用,因此我们无法将字段y从借用上下文中移出,我们也无法复制该字段,因为可变引用不是{{1 }}。

我们可以通过强制将闭包称为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

答案 1 :(得分:6)

考虑以下代码:

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具有移动语义,有关更多详细信息,请参见Copy/move semantics documentation of &T/&mut T types itself

缺少的东西

编译器抛出与生存期相关的问题:

cannot infer an appropriate lifetime for borrow expression due to conflicting requirements

但是,正如Sven Marnach所指出的,还有一个问题与禁止从借来的内容中移出有关。

但是为什么编译器不会抛出此错误?即:

cannot move out of borrowed content

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

答案很长

闭包由两部分组成:

  • 闭包的状态:包含闭包捕获的所有对象的结构

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

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

遇到参考时,锈控制两个方面:

  1. 状态:如果引用指向有效的内存片,(即读取生存期有效性);

  2. 逻辑:如果内存片是别名的,换句话说,如果它同时从多个引用中指向;

请注意,为避免内存混淆,禁止从借用内容中移出。

rustc编译器通过stages(这里是简化的工作流程)执行工作:

.rs input -> AST -> HIR -> hir postprocessing -> MIR -> mir postprocessing -> llvm IR -> binary

编译器报告了生命周期问题,因为rustc首先在hir postprocessing中执行类型检查阶段,该阶段包括生命周期analisys,然后,如果成功,则在mir postprocessing阶段执行借位检查

答案 2 :(得分:5)

(目前,这只是有根据的猜测。如果人们似乎同意这个答案是正确的,我将删除此免责声明)


这里有两个主要作用:

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

1。闭包返回对环境的引用

闭包不能返回寿命为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的引用(即:对其捕获的环境的引用)。 / p>

只需确保:闭包可以捕获引用并将其返回!他们可以通过引用捕获并返回该引用。那些事情有些不同。它与闭包类型中存储的内容有关。如果类型中存储了引用,则可以将其返回。但是我们不能返回对封闭类型中存储的任何内容的引用。

嵌套可变引用

考虑此函数(请注意,参数类型暗示'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。