Rust:内部变量对外部PineMap的借用为何超出自身作用域?
问题背景
我想用pinus::sync::PineMap实现:让所有等价对象的引用指向内存中的同一实例(实例由PineMap持有)。选PineMap是因为它的insert方法只借用&self,不会移动元素,原有值的引用始终有效,还能构建自引用项。
问题代码
#[derive(Eq, PartialEq, Ord, PartialOrd)] enum List<'a> { Cons(isize, &'a List<'a>), Nil, } fn main() { use List::*; use pinus::{prelude::*, sync::PineMap}; let mut table = PineMap::new(); table.insert(Nil, Nil); { let nil = table.get(&Nil).unwrap(); table.insert(Cons(1, nil), Cons(1, nil)); } table.clear(); }
报错信息
error[E0597]: `table` does not live long enough --> src/main.rs:13:19 | 13 | let nil = table.get(&Nil).unwrap(); | ^^^^^^^^^^^^^^^ borrowed value does not live long enough ... 17 | } | - | | | `table` dropped here while still borrowed | borrow might be used here, when `table` is dropped and runs the `Drop` code for type `PineMap` error[E0502]: cannot borrow `table` as mutable because it is also borrowed as immutable --> src/main.rs:16:5 | 13 | let nil = table.get(&Nil).unwrap(); | --------------- immutable borrow occurs here ... 16 | table.clear(); | ^^^^^^^^^^^^^ mutable borrow occurs here 17 | } | - immutable borrow might be used here, when `table` is dropped and runs the `Drop` code for type `PineMap`
疑问
我原本以为把nil放在内部作用域就能解决生命周期问题,毕竟它的生命周期应该比table短,table销毁时不该还借用它,但实际不是这样。为什么内部变量对外部变量的借用会超出自身作用域?另外,如果这个方案不可行,怎么实现基于对象等价性的引用缓存:创建对象O时先查是否存在,存在就取缓存实例的引用,不存在就缓存再取引用?
解答
为什么内部作用域没解决问题?
问题出在你把Cons(1, nil)插入到PineMap里的时候,这个Cons持有了nil的引用——而nil是指向table中Nil的引用。这意味着PineMap内部存储的Cons(1, nil)实际上持有了对自身的引用(因为Nil也存在于同一个table里)。
Rust的借用检查器会认为,只要table里存在这样的自引用,那么table的整个生命周期内都存在这个借用关系。即使你把nil放在内部作用域,table里的Cons条目依然持有这个引用,所以借用并没有随着内部作用域结束而失效。当你调用table.clear()或者table被销毁时,借用检查器会发现内部还有未结束的借用,从而报错。
可行的替代方案
要实现基于等价性的引用缓存,你需要绕过Rust的静态生命周期检查,通常可以用引用计数结合内部可变性来实现,比如用Rc(单线程)或Arc(多线程)配合哈希表:
方案1:单线程场景用Rc + HashMap
use std::collections::HashMap; use std::rc::Rc; #[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Clone)] enum List { Cons(isize, Rc<List>), Nil, } struct Cache { map: HashMap<List, Rc<List>>, } impl Cache { fn new() -> Self { let mut map = HashMap::new(); let nil = Rc::new(List::Nil); map.insert(List::Nil, nil.clone()); Self { map } } fn get_or_insert(&mut self, item: List) -> Rc<List> { if let Some(existing) = self.map.get(&item) { existing.clone() } else { let rc_item = Rc::new(item); self.map.insert(rc_item.clone(), rc_item.clone()); rc_item } } } fn main() { let mut cache = Cache::new(); let nil = cache.get_or_insert(List::Nil); let cons1 = cache.get_or_insert(List::Cons(1, nil.clone())); // 再次获取Cons(1, nil)会拿到同一个Rc实例 let cons1_dup = cache.get_or_insert(List::Cons(1, nil)); assert!(Rc::ptr_eq(&cons1, &cons1_dup)); }
方案2:多线程场景用Arc + DashMap
如果需要线程安全,可以用Arc配合线程安全的哈希表,比如dashmap:
use dashmap::DashMap; use std::sync::Arc; #[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Clone)] enum List { Cons(isize, Arc<List>), Nil, } struct Cache { map: DashMap<List, Arc<List>>, } impl Cache { fn new() -> Self { let mut map = DashMap::new(); let nil = Arc::new(List::Nil); map.insert(List::Nil, nil.clone()); Self { map } } fn get_or_insert(&self, item: List) -> Arc<List> { // 先尝试查找 if let Some(entry) = self.map.get(&item) { entry.value().clone() } else { // 不存在则插入,避免竞态 let arc_item = Arc::new(item); self.map.insert(arc_item.clone(), arc_item.clone()); arc_item } } } fn main() { let cache = Cache::new(); let nil = cache.get_or_insert(List::Nil); let cons1 = cache.get_or_insert(List::Cons(1, nil.clone())); let cons1_dup = cache.get_or_insert(List::Cons(1, nil)); assert!(Arc::ptr_eq(&cons1, &cons1_dup)); }
方案说明
Rc/Arc是引用计数智能指针,允许多个所有者共享同一个实例,不需要依赖静态生命周期。- 哈希表中存储的是
Rc/Arc包装的实例,获取缓存时只是克隆一个指针(成本极低),而非复制整个对象。 - 这种方式避免了自引用带来的生命周期问题,因为
Rc/Arc的引用是动态管理的,不受静态借用规则约束。
内容的提问来源于stack exchange,提问作者user3310334

