如何创建可递归借用父级/创建者的Rust struct?
基础可行案例
我先实现了一个可行的基础模式:工厂类型Foo创建Bar实例,每个Bar借用它的创建者Foo,目的是Bar被drop时自动操作父级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 } // y被drop时会递增x.i x.print(); // x:1 }
递归化尝试(遇到问题)
现在我想把这个模式递归化:让子实例也能创建新的同类型实例(即不再区分Foo和Bar,统一用Foo类型),每个新创建的Foo引用回自身的父级,且父级必须比子级存活更久。这是我多次尝试后的代码:
struct Foo<'a, 'b> where 'b: 'a { parent: Option<&'a mut Foo<'b, 'b>>, // 注释1 s: String, i: u64, } impl<'a, 'b> Foo<'a, 'b> { fn new(s: String) -> Foo<'a, 'b> { // 注释2 Foo { parent: None, s, i: 0 } } fn push(&'b mut self, s: String) -> Foo<'a, 'b> { // 注释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()); // 错误1 y.print(); // y:0 } // y被drop时应递增x.i x.print(); // x:1, 错误2 }
注释与错误详情
- 注释1:在
Foo<'b, 'b>中重复使用相同生命周期感觉不合理,因为我并不想表示这个Foo的两个生命周期必须匹配。但用'a也不对,如果指定新生命周期又要给Foo加更多参数,陷入递归问题。 - 注释2:这些函数签名里返回类型用的生命周期没在其他地方出现,感觉不对,但不知道怎么改。
- 错误1:提示“借用的值存活时间不足”以及“
x在此处被销毁时仍被借用”。我期望y对x的引用只存活到y被销毁,但Rust不认可这个约束。 - 错误2:提示“无法将
x借用为不可变,因为它还被可变借用”,这是同一个问题的另一种表现。我期望y里对x的可变借用到调用x.print()时已经失效,但编译器不这么认为。
补充说明
我查的大部分解决方案都是关于循环引用(父级持有子级列表,子级引用父级),但我不需要这个逻辑:我的父级不持有子级的引用,只有子级到父级的单向引用。我也看过Rust规范,但难以理解,还没完全掌握生命周期的工作原理,所以没法跟进细节。
我希望借助Rust的优势验证父级实例比子级存活更久,但当父级和子级是同一类型时,不知道怎么向编译器表达这个需求,希望有人能告诉我怎么实现,或者为什么不可行。
解答
问题核心是用单一类型实现“子实例持有父实例的可变引用,且父实例存活更久”,当前的生命周期参数设计没有正确表达父子实例的生命周期关联。
为什么当前代码失败?
Foo<'a, 'b>的生命周期约束'b: 'a没有正确关联子实例和父实例的生命周期。new函数返回的Foo<'a, 'b>中,'a和'b是未约束的泛型参数,编译器无法推断出它们与实际变量的生命周期关系,导致它认为x的可变引用被y永久持有,直到x销毁,从而触发错误。
正确的实现方式
需要让Foo的生命周期参数仅表示它对父级的引用的生命周期,同时确保子实例的生命周期严格限制在父实例的可变借用范围内。修改后的代码如下:
struct Foo<'parent> { parent: Option<&'parent mut Foo<'parent>>, s: String, i: u64, } impl<'parent> Foo<'parent> { // 根节点没有父引用,用'static表示无外部借用 fn new(s: String) -> Foo<'static> { Foo { parent: None, s, i: 0 } } // 创建子实例:子实例的生命周期'child必须短于父实例的可变借用周期'parent fn push<'child>(&'child mut self, s: String) -> Foo<'child> where 'child: 'parent, { Foo { parent: Some(self), s, i: 0 } } fn print(&self) { println!("{}:{}", self.s, self.i); } } impl<'parent> Drop for Foo<'parent> { 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 // 可以继续创建子节点 { let z = y.push("z".to_string()); z.print(); // z:0 } // z被drop,y.i +=1 y.print(); // y:1 } // y被drop,x.i +=1 x.print(); // x:1 }
为什么这样能工作?
- 生命周期参数简化:
Foo<'parent>只需要一个生命周期参数,表示它对父级的引用的生命周期。 - 根节点的
'static生命周期:根节点没有父引用,用'static表示它不依赖任何外部借用(编译器会自动适配其实际局部生命周期)。 push方法的约束:'child: 'parent确保子实例的生命周期不会超过父实例的可变借用周期,编译器可以明确验证子实例一定先于父实例被销毁,保证引用安全。- Drop顺序正确:子实例先被销毁,触发父实例的计数增加,完全符合需求。
关于“递归类型”的说明
Foo<'parent>引用Foo<'parent>是合法的,因为生命周期参数是协变的,编译器可以处理这种递归结构,只要生命周期约束清晰,就不会出现无限递归问题。
内容的提问来源于stack exchange,提问作者Jayonas

