繁体   English   中英

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

[英]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存储对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 bound 的泛型函​​数。 这段代码也能正常工作:

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. 对可变引用的可变引用只能使用外部引用的生命周期(与不可变引用不同)

返回对环境的引用的闭包

闭包不能在self (闭包对象)的生命周期内返回任何引用 这是为什么? 每个闭包都可以称为FnOnce ,因为这是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借来的”)。 如上所述,闭包不能做到这一点。

所以编译器不能为我们生成闭包实现。

考虑这个代码:

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 trait 的实现

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

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

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM