如何借用 RefCell<HashMap>,查找一个键,并返回结果的引用?

16

我有一个RefCell<HashMap>,想要借用这个表格,查找一个键,并返回结果的引用:

use std::cell::RefCell;
use std::collections::HashMap;

struct Frame {
    map: RefCell<HashMap<String, String>>,
}

impl Frame {
    fn new() -> Frame {
        Frame {
            map: RefCell::new(HashMap::new()),
        }
    }

    fn lookup<'a>(&'a self, k: &String) -> Option<&'a String> {
        self.map.borrow().get(k)
    }
}

fn main() {
    let f = Frame::new();
    println!("{}", f.lookup(&"hello".to_string()).expect("blargh!"));
}

如果我删除 RefCell,那么一切都正常:

(playground)
struct Frame {
    map: HashMap<String, String>,
}

impl Frame {
    fn lookup<'a>(&'a self, k: &String) -> Option<&'a String> {
        self.map.get(k)
    }
}

如何在不复制哈希表中的字符串的情况下编写正确的查找函数?

1个回答

19
当您从RefCell中借用时,您获得的引用的生命周期比RefCell的短。这是因为引用的生命周期受borrow()返回的guard限制。该guard确保在guard被删除之前没有其他人可以对该值进行可变引用。
但是,您正在尝试返回一个值而不保持guard处于活动状态。如果Frame有一个采用&self参数但尝试改变map的方法(如果使用RefCell则可能会发生这种情况-如果不需要,请放弃使用RefCell并在修改map的方法上编写&mut self),您可能会意外地破坏某个其他人拥有引用的String。这正是借用检查器设计的报告此类错误的原因!
如果map值在实质上是不可变的(即您的类型不允许更改map的值),则还可以在map中将它们包装在Rc中。因此,您可以返回Rc<String>的克隆(这仅克隆引用计数指针,而不是底层字符串),这样可以在从函数返回之前释放map上的借用。
struct Frame {
    map: RefCell<HashMap<String, Rc<String>>>
}

impl Frame {
    fn lookup(&self, k: &String) -> Option<Rc<String>> {
        self.map.borrow().get(k).map(|x| x.clone())
    }
}

谢谢。这是一个很好的解释。我还在学习,所以我一直在与借用检查器搏斗,但是当然这是有道理的。我想我还不太明白RefCell为什么有所区别——为什么非RefCell版本也不会因为同样的原因而引起问题呢? - Marc Miller
@MarcMiller:我想你现在已经意识到Rust的所有权和借用是非常重要的。通常情况下,这些都会在编译时进行检查,但是基于“unsafe”代码的构造可以欺骗编译器,并在运行时验证正确性。RefCell就是这样一种构造,因此引入它会稍微改变规则;你可以将其视为单线程代码的读写互斥锁。 - Matthieu M.
@matthieu-m:感谢您的参与!能否请您澄清一下?您是说RefCell将其更改为一种类似于写入锁而不仅仅是读取锁,即使我没有进行borrow_mut吗?我真的不想在这里克隆我的返回值,因为我的对象可能非常大,所以我正在考虑做一些像引用计数的事情:RefCell<HashMap<String,Rc<String>> - 但这让我觉得我在做错事。谢谢。 - Marc Miller
@MarcMiller:如果你根本没有使用borrow_mut(),那么使用RefCell就毫无意义了。直接将HashMap存储在你的Frame中即可。如果你想避免复制/克隆,那么返回一个引用,这就是它们的目的! - Francis Gagné
1
@MarcMiller: RefCell 本质上是一种读写锁,虽然不是线程安全的;当你调用.borrow_mut()方法时,它会检查没有其他人已经持有借用(写入需要独占访问权限),而当你调用.borrow()方法时,它会检查没有写入操作进行中(读者获得共享访问权限)。为了确保RefCell具有写入专属性,这意味着通过.borrow()方法借用的内容不能比守卫(Ref<'a, T>)活得长,这就是你在这里遇到的问题(因为守卫的生命周期不长于函数)。返回一个Rc<String>已经被证明可以解决这个问题。 - Matthieu M.

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