我该如何创建一个递归借用其父/创建者的结构体?

3

我需要一些帮助来理解如何指定生命周期,以便让Rust理解我的意图(或者如果不可能,为什么不行)。

首先,这是我的基本情况,它可以正常工作。想象一个工厂类型Foo,它创建Bar,每个Bar都借用了创建它的Foo。借用的目的是当一个Bar被丢弃时,它可以自动操作创建它的Foo。显然,Foo必须比Bar存在更长的时间,这正是我的意图,也是我希望编译器验证的内容。

struct Foo {
    s: String,
    i: u64,
}

struct Bar<'a> {
    parent: Option<&'a mut Foo>,
    s: String,
    i: u64,
}

impl Foo {
    fn new(s: String) -> Foo {
        Foo { s, i: 0 }
    }

    fn push<'a>(&'a mut self, s: String) -> Bar<'a> {
        Bar { parent: Some(self), s, i: 0 }
    }

    fn print(&self) {
        println!("{}:{}", self.s, self.i)
    }
}

impl<'a> Bar<'a> {
    fn print(&self) {
        println!("{}:{}", self.s, self.i)
    }
}

impl<'a> Drop for Bar<'a> {
    fn drop(&mut self) {
        if let Some(parent) = &mut self.parent {
            parent.i += 1;
        }
    }
}

fn main() {
    let mut x = Foo::new("x".to_string());
    {
        let y = x.push("y".to_string());
        y.print(); // y:0
    } // when y is dropped it increments x.i
    x.print(); // x:1
}

现在我希望将这个模式递归,以便y可以推入一个新的z(其中y必须像预期的那样比z更长寿),以此类推。换句话说,我想要的是不再有单独的FooBar,而是要有一个单一的类型Foo,它的push方法会创建新的指向它的Foo。经过数小时的尝试,以下是我想到的最佳方案:

struct Foo<'a, 'b> where 'b: 'a {
    parent: Option<&'a mut Foo<'b, 'b>>, // note 1
    s: String,
    i: u64,
}

impl<'a, 'b> Foo<'a, 'b> {
    fn new(s: String) -> Foo<'a, 'b> { // note 2
        Foo { parent: None, s, i: 0 }
    }

    fn push(&'b mut self, s: String) -> Foo<'a, 'b> { // note 2
        Foo { parent: Some(self), s, i: 0 }
    }

    fn print(&self) {
        println!("{}:{}", self.s, self.i);
    }
}

impl<'a, 'b> Drop for Foo<'a, 'b> {
    fn drop(&mut self) {
        if let Some(parent) = &mut self.parent {
            parent.i += 1;
        }
    }
}

fn main() {
    let mut x = Foo::new("x".to_string());
    {
        let y = x.push("y".to_string()); // error 1
        y.print(); // y:0
    } // when y is dropped it increments x.i
    x.print(); // x:1, error 2
}

注意事项和错误详情:

  • 注意1:在Foo<'b, 'b>中两次使用相同的生命周期感觉不对,因为我并不想表达那个Foo的两个生命周期需要匹配。然而,对于任何一个生命周期使用'a也不对,而且我不能指定一个新的/不同的生命周期而不添加另一个生命周期参数到Foo中,这反过来又迫使我在这里指定第三个生命周期,从而导致我遇到了同样的问题。看来这是递归的。只是为了好玩,我查找了一下Rust中是否有C++可变参数模板的等价物,但即使可能,它仍然感觉像是一个糟糕的解决方案。
  • 注意2:这些行看起来不对,因为我在返回类型中使用了生命周期,在函数签名中没有出现,但我不知道该怎么办。
  • 错误1:错误是“借用的值不够长”和“在这里放弃了x的引用,但仍然被借用”(其中“这里”是最后一行)。我的期望是yx的引用只在y的生命周期内存在,但Rust显然认为生命周期不是这样工作的,所以似乎我没有告诉编译器如何约束(或不约束)生命周期。
  • 错误2:这里的错误是“无法将x作为不可变借用,因为它也被作为可变借用”,这只是同样问题的另一个症状。我期望在调用x.print()时,y中的可变借用已经消失了,但编译器并没有这样认为。

当我寻找解决此问题的方法时,我发现很多都是关于创建循环引用的,其中父项有一个子项列表,每个子项都指向父项。我理解为什么这很困难/不可能,但我不认为这就是我在这里要做的事情。我的父/工厂类型不维护对其子项/小部件的引用;引用只沿着一个方向,从子项/小部件到父项/工厂。

我也发现自己阅读了与此相关的Rust规范的部分,但老实说,我已经超出了我的能力范围。至少有一个关于生命周期实际上如何工作的顿悟时刻我还没有,这使得我很难跟进所有规范级别的细节,更不用说它们与我的问题有关了。

我觉得我想让Rust做它擅长的事情--验证父项是否比子项更长寿--但我不确定如何向Rust解释当父项和子项具有相同类型时我要做什么。有人可以帮助我理解如何做到这一点或者为什么不能这样做吗?


4
这里我认为的陷阱是对引用的误用。在单向链表中,每个节点应该拥有下一个节点。 - xiao
4
请注意,这将与不可变引用一起正常工作。您遇到的问题是可变引用的不变性。 - Peter Hall
3
我相当确定你无法使用可变引用使其工作。你需要使用引用计数。 - Peter Hall
2
尝试使用不可变引用与内部可变性(例如CellRwLock等)相结合。我认为您可以只使用单个生命周期并依赖协变性? - Solomon Ucko
2
Foo::pushself 参数类型为 &'b mut Foo<'_, 'b>。关于为什么这从来不是一个好主意的解释,请参见此答案 - Jmb
显示剩余2条评论
1个回答

1

为了后代,我采用了@SolomonUcko在评论中建议的解决方案——使用具有内部可变性的不可变父引用。以下是我问题中简化示例的已解决版本:

use std::cell::Cell;

struct Foo<'a> {
    parent: Option<&'a Foo<'a>>,
    s: String,
    i: Cell<u64>, // field manipulated by child uses interior mutability
}

impl<'a> Foo<'a> {
    fn new(s: String) -> Self {
        Foo { parent: None, s, i: Cell::new(0) }
    }

    fn push(&'a self, s: String) -> Self { // non-mut self
        Foo { parent: Some(self), s, i: Cell::new(0) }
    }

    fn print(&self) {
        println!("{}:{}", self.s, self.i.get());
    }
}

impl<'a> Drop for Foo<'a> {
    fn drop(&mut self) {
        if let Some(parent) = &mut self.parent {
            parent.i.replace(parent.i.get() + 1); // mutate parent's Cell contents
        }
    }
}

fn main() {
    let x = Foo::new("x".to_string());
    {
        let y = x.push("y".to_string());
        {
            let z = y.push("z".to_string());
            z.print(); // z:0
        } // z dropped, increments y.i
        y.print(); // y:1
    } // y dropped, increments x.i
    x.print(); // x:1
}

我实际上使用这个模式的代码还有一个第三方类型的字段,它只是一个围绕着 Arc (即引用计数)的包装器,并且从子级操纵它也能正常工作,正如 @PeterHall 在另一条评论中建议的那样。


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