如何解决 Rust 中可变引用的生命周期错误?

4

我不确定为什么以下代码无法编译。

use std::cmp::Ordering;

struct MyItr<'a> {
    cur: &'a i32,
}

impl<'a> Ord for MyItr<'a> {
    fn cmp(&self, other: &MyItr) -> Ordering {
        self.cur.cmp(&other.cur)
    }
}

impl<'a> PartialOrd for MyItr<'a> {
    fn partial_cmp(&self, other: &MyItr<'a>) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl<'a> PartialEq for MyItr<'a> {
    fn eq(&self, other: &MyItr) -> bool {
        self.cur == other.cur
    }
}

impl<'a> Eq for MyItr<'a> {}

fn f0<'a>(t0: &'a mut MyItr<'a>, t1: &'a mut MyItr<'a>, i: &'a i32) {
    let t = std::cmp::max(t0, t1);
    t.cur = i;
}

fn f1() {
    let i0 = 1;
    let i1 = 2;
    let mut z0 = MyItr { cur: &i0 };
    let mut z1 = MyItr { cur: &i1 };

    let i2 = 3;
    f0(&mut z0, &mut z1, &i2);
}

$ cargo build
   Compiling foo v0.1.0 (file:///private/tmp/foo)
error: `z1` does not live long enough
  --> lib.rs:40:1
   |
39 |     f0(&mut z0, &mut z1, &i2);
   |                      -- borrow occurs here
40 | }
   | ^ `z1` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

我理解的是,当 f0 调用结束时,对 z0z1 的借用引用会被撤销。然而,编译器似乎假定这些借用引用没有被撤销。

$ cargo --version
cargo 0.20.0-nightly (41e490480 2017-05-16)

1
在这个上下文中,“backed”不是一个有意义的词。我不明白你认为应该发生什么。 - DK.
我的理解是:在调用f0()之后,借用z0和z1的过程已经结束,因此我不需要保留z0和z1实例。 - ruimo
1
更清晰的措辞应该是“借用的引用被丢弃”或“借用结束”。 - DK.
1个回答

10
这里存在两个问题。首先,您已经过度指定了生命周期,导致编译器无法处理。
fn f0<'a>(t0: &'a mut MyItr<'a>, t1: &'a mut MyItr<'a>, i: &'a i32)

你已经告诉编译器所有的参数必须是具有“相同”生命周期的指针。编译器可以缩小重叠的生命周期,但在这种情况下无济于事。你已经指定了指向的指针与它们所指的东西具有相同的生命周期,并且外部指针是可变的。
第二个问题是(即使修复了第一个问题),你试图做的事情是不安全的,并将导致悬挂指针。
以下是一个更简单的例子:
struct S<'a> {
    ptr: &'a i32,
}

fn f<'b>(t: &'b mut S<'b>, new_ptr: &'b i32) {}

fn main() {
    let i0 = 1;
    let mut s = S { ptr: &i0 };

    let i1 = 2;
    f(&mut s, &i1);
}

什么是'b?编译器只能缩短生命周期,因此通常你只需取所有传递内容的生命周期并选择最短的那个。在这种情况下,它将是i1的生命周期。所以,它必须缩短&s的生命周期。指向s的指针本身的生命周期不是问题(可以缩短借用时间),但缩短内部生命周期(用于ptr字段)就是一个问题。
如果编译器缩短了s.ptr的生命周期,那么您将能够将&i1存储在该字段中。s期望s.ptr比自己更长寿,但这将不再成立:意味着i1将在s之前被销毁,从而导致s.ptr包含一个悬空指针。 Rust不允许发生这种情况。
因此,Rust无法缩小s的内部'a生命周期...但如果它无法缩小它,那么这意味着'b必须是完整的、未缩短的'a。但等等,这意味着'b的寿命比s本身和i1的寿命都要长。这是不可能的。
因此失败了。
解决方案需要两件事。首先,您不需要过度指定生命周期。其次,您需要确保某些有效寿命存在;在原始代码的情况下,这意味着将i2移动到z0z1上面,以便它比它们更长寿。像这样:
fn f0<'a>(t0: &mut MyItr<'a>, t1: &mut MyItr<'a>, i: &'a i32) {
    let t: &mut MyItr<'a> = std::cmp::max(t0, t1);
    t.cur = i;
}

fn f1() {
    let i0 = 1;
    let i1 = 2;
    let i2 = 3;
    let mut z0 = MyItr { cur: &i0 };
    let mut z1 = MyItr { cur: &i1 };

    f0(&mut z0, &mut z1, &i2);
}

一个经验法则:不要在所有地方都使用同一个lifetime。只有当需要相同的时候才使用相同的lifetime。


感谢详细的解释! - ruimo
1
@ruimo 如果你想阅读更偏向类型理论的解释,这里有一篇Rustonomicon页面专门介绍生命周期子类型。特别地,编译器在&'a mut S<'b>中可以缩小'a但不能缩小'b的原因是&'a mut T'a具有协变性而对T具有不变性。这帮助我更好地学习和理解生命周期。 - trent
1
总的来说,“不要在所有地方都滥用单一生命周期”这个规则会让你走得更远。 - trent

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接