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

sou*_*ics 7 closures reference mutable lifetime rust

当我遇到这个有趣的场景时,我正在玩Rust关闭:

fn main() {
    let mut y = 10;

    let f = || &mut y;

    f();
}
Run Code Online (Sandbox Code Playgroud)

这给出了一个错误:

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();
  |     ^^^
Run Code Online (Sandbox Code Playgroud)

即使编译器试图逐行解释它,我仍然不明白它到底在抱怨什么.

是否试图说可变引用不能比封闭的封闭寿命更长?

如果我删除了呼叫,编译器不会抱怨f().

Luk*_*odt 7

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


这里有两件主要的事情:

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

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

闭包不能返回具有self(闭包对象)生命周期的任何引用.这是为什么?每个闭包都可以被称为FnOnce,因为这是超特性,FnMut而超特性反过来又是超特征Fn.FnOnce有这个方法:

fn call_once(self, args: Args) -> Self::Output;
Run Code Online (Sandbox Code Playgroud)

请注意,它self是按值传递的.因此,既然self消耗了(现在生活在call_once函数中),我们就不能返回对它的引用 - 这相当于返回对本地函数变量的引用.

理论上,call_mut它将允许返回引用self(因为它接收&mut self).但由于call_once,call_mutcall都以相同的机身上实现,在一般封闭无法返回引用self(即:其捕获环境).

只是为了确定:闭包可以捕获引用并返回它们!他们可以通过引用捕获并返回该引用.那些东西是不同的东西.它只是存储在闭包类型中的内容.如果类型中存储了引用,则可以返回该引用.但是我们不能返回对闭包类型中存储的任何内容的引用.

嵌套的可变引用

考虑这个函数(请注意,参数类型暗示'inner: 'outer; 'outer小于'inner):

fn foo<'outer, 'inner>(x: &'outer mut &'inner mut i32) -> &'inner mut i32 {
    *x
}
Run Code Online (Sandbox Code Playgroud)

这不会编译.乍一看,它似乎应该编译,因为我们只是剥离了一层参考.它确实适用于不可变引用!但是可变参考在这里是不同的以保持健全性.

但是,回归&'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();
Run Code Online (Sandbox Code Playgroud)

返回的参考文献应该有多长时间?

  • 它不可能'a,因为我们基本上有一个&'s mut &'a mut i32.并且如上所述,在这种嵌套的可变参考情况下,我们无法提取更长的寿命!
  • 但它也不可能,'s因为这意味着关闭会返回一些具有生命周期的东西'self("借来self").如上所述,闭包不能这样做.

所以编译器无法为我们生成闭包impls.

  • 我认为真正的原因是可变引用不是“复制”,而不可变引用是,我们不能将可变引用移出借用的上下文。不过,这不是错误消息所说的。 (2认同)

Sve*_*ach 7

精简版

闭包f存储了一个可变引用y.如果允许返回此引用的副本,则最终会有两个同时发生的可变引用y(一个在闭包中,一个返回),这是Rust的内存安全规则禁止的.

长版

关闭可以被认为是

struct __Closure<'a> {
    y: &'a mut i32,
}
Run Code Online (Sandbox Code Playgroud)

因为它包含一个可变引用,所以闭包被称为FnMut,基本上是定义

fn call_mut(&mut self, args: ()) -> &'a mut i32 { self.y }
Run Code Online (Sandbox Code Playgroud)

因为我们只有一个对闭包本身的可变引用,所以我们不能将该字段y移出借用的上下文,也不能复制它,因为可变引用不是Copy.

我们可以强制编译器接受代码,强制关闭被调用FnOnce而不是FnMut.这段代码工作正常:

fn main() {
    let x = String::new();
    let mut y: u32 = 10;
    let f = || {
        drop(x);
        &mut y
    };
    f();
}
Run Code Online (Sandbox Code Playgroud)

由于我们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();
}
Run Code Online (Sandbox Code Playgroud)


att*_*ona 6

考虑以下代码:

fn main() {
    let mut y: u32 = 10;

    let ry = &mut y;
    let f = || ry;

    f();
}
Run Code Online (Sandbox Code Playgroud)

它的工作原理是因为编译器能够推断出它ry的生命周期:引用ry存在于相同的范围内y.

现在您的代码的等效版本:

fn main() {
    let mut y: u32 = 10;

    let f = || {
        let ry = &mut y;
        ry
    };

    f();
}
Run Code Online (Sandbox Code Playgroud)

现在,编译器将指定ry与闭包体范围相关联的生命周期,而不是与主体关联的生命周期.

另请注意,不可变参考案例有效:

fn main() {
    let mut y: u32 = 10;

    let f = || {
        let ry = &y;
        ry
    };

    f();
}
Run Code Online (Sandbox Code Playgroud)

这是因为&T具有复制语义并&mut T具有移动语义,请参阅 &T /&mut T类型本身的复制/移动语义文档以获取更多详细信息.

缺少的一块

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

cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
Run Code Online (Sandbox Code Playgroud)

但正如Sven Marnach指出的那样,还有一个问题与禁止从借来的内容中撤出有关.

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

cannot move out of borrowed content
Run Code Online (Sandbox Code Playgroud)

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

答案很长

封盖由两部分组成:

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

  • 逻辑封闭的:的一种实现FnOnce,FnMutFn性状

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

当参考遇到锈蚀控制两个方面:

  1. 状态:如果参考指向一个有效的存储器片,(即读寿命有效性);

  2. 逻辑:如果如果同时从一个以上的参考指向的存储器片是锯齿,换言之;

请注意,禁止从借来的内容中移出以避免内存别名.

HIR postprocessing编译器通过阶段执行他的工作,这里是一个简化的工作流:

.rs input -> AST -> HIR -> HIR postprocessing -> MIR -> HIR postprocessing -> LLVM IR -> binary
Run Code Online (Sandbox Code Playgroud)

编译器报告生命周期问题,因为MIR postprocessing首先执行类型检查阶段ry,其中包括生命周期分析,之后,如果成功,则执行借入检查ry阶段.