Rust:生命周期使用不当

3
我测试了一个包含生命周期的小例子,但有时我无法理解问题,比如这里:
struct Space<'r> {
    v: &'r mut usize,
    z: usize,
}

impl<'r> Space<'r> {
    fn new(v: &'r mut usize) -> Self {
        Self { v, z: 0 }
    }
    
    fn add(&'r mut self) {
        self.z += 1;
        *self.v = self.z
    }

    fn make_val(&'r mut self, v: u32) -> Val<'r> {
        Val::new(self, v)
    }
}

struct Val<'r> {
    space: &'r mut Space<'r>,
    v: u32,
}

impl<'r> Val<'r> {
    fn new(space: &'r mut Space<'r>, v: u32) -> Self {
        Self { space, v }
    }
}

impl<'r> Clone for Val<'r> {
    fn clone(&self) -> Self {
        self.space.add();
        
        Self { space: self.space, v: self.v }
    }
}

fn main() {
    let mut v = 0usize;
    let mut space = Space::new(&mut v);
    let mut val1 = space.make_val(1234);
    let mut val2 = val1.clone();
    
    println!("Res: {} - {}/{}", v, val1.v, val2.v);
}

(Playground)

出现了两个编译错误,第一个在第34行的错误非常奇怪,它说生命周期无法超过方法调用是内部的,没有任何东西被传递到外面。第36行的错误对我来说很难解决。

请问你能解释一下这些问题,并告诉我如何解决吗?


在你的 clone 方法中,你是否打算修改原始值? - Solomon Ucko
无论如何,您都无法实现可变引用的克隆。 您可以尝试使用 RefCell 来延迟到运行时进行借用,但是 Rust 的方法是尽可能避免使用引用。 在您的示例中,我没有任何特定原因不使用拥有值。 - Alexey Larionov
1个回答

4

在我看来,需要着重关注的部分是,以下是我可能会做出的更改:

结构体

从你提供的两个结构体中,第一个看起来很合理。

struct Space<'r> {
    v: &'r mut usize,
    z: usize,
}

然而,对于我来说,Val 结构并不是很有意义。如果引用 Space 的生命周期 'rSpace 本身相同,则将 Val<'r> 移入 Space<'r> 本质上是等效的。虽然这不是非法的,但我建议进行更改。
// Just claim Space<'r> completely
struct Val<'r> {
    space: Space<'r>,
    v: u32,
}

另一个选择是给Val<'r>增加一个额外的生命周期,这样可以多次调用make_val而不会导致生命周期冲突。

// life of reference to space 'a <= 'r
struct Val<'a, 'r: 'a> {
    space: &'a mut Space<'r>,
    v: u32,
}

// Modify to allow for second lifetime to be used.
fn make_val<'a>(&'a mut self, v: u32) -> Val<'a, 'r> {
    Val { space: self, v }
}

Space::add

函数运行时&'r mut self的显式生命周期是不必要的。当尝试运行代码时,你会得到两个错误,第一个完整的错误只是编译器告诉你为什么这样行不通。它过度限制了函数并且不必要地让编译器感到困惑,因此我们可以将其删除。

fn add(&mut self) {
    self.z += 1;
    *self.v = self.z
}

它无法解决的原因是 &mut self 实际上包含两个需要相等的生命周期。如果我们将涉及到的所有生命周期可视化,可能会更容易理解:
// add(&mut self) tells the compiler to infer a lifetime for 'a that doesn't exceed 'b
fn add<'a, 'b: 'a>(self: &'a mut Space<'b>) {}

// add(&'r mut self) tells the compiler that the reference must live as long as the data 
fn add<'b>(self: &'b mut Space<'b>) {}

克隆

克隆包含可变引用的东西没有意义。Rust非常明确地禁止在任何给定内存点上有多个可变引用的存在。技术上说,克隆引用是可能的,但需要使用不安全的代码。您永远不应该这样做,但我仍然在下面编写了示例代码。

impl<'r> Clone for Val<'r> {
    fn clone(&self) -> Val<'r> {
        unsafe {
            // Make self mutable so we can access self.space
            let my_self: &mut Self = &mut *(self as *const _ as *mut _);

            // Requires a mutable reference to self to run space::add
            my_self.space.add();
        
            Self {
                // Duplicate reference via unsafe code
                space: &mut *my_self.space,
                v: self.v
            }
        }
    }
}

打印

最后,在主函数中的输出语句使用了 v,尽管对其进行可变引用的操作仍在进行中。这是相当容易理解的,所以我不会再详细解释。

安全的Rust

解决这个问题的安全方法是使用Rc<RefCell<T>>Rc允许在单个线程中获取多个拥有权,而RefCell则允许内部可变性。RefCell能够做到这一点是因为它在运行时检查是否存在其他引用,而非编译时。这里有一个版本的playground,它使用了单个RefCell,但也可以使用第二个来更好地匹配您的原始代码。


感谢您的解释和解决方案。 - Corebreaker
复制引用在技术上是可行的,但是据我所知,这将始终是insta-UB。 - Cerberus

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