使用原始指针访问 RefCell<HashMap> 的 &T 是否安全<T>?



我有一个类似缓存的结构,它在内部使用HashMap

impl Cache {
    fn insert(&mut self, k: u32, v: String) {
        self.map.insert(k, v);
    }
    fn borrow(&self, k: u32) -> Option<&String> {
        self.map.get(&k)
    }
}

具有外部可变性的游乐场

现在我需要内部可变性。由于HashMap没有实现Copy,我的猜测是RefCell是要遵循的道路。编写insert方法很简单,但我遇到了借用函数的问题。我可以返回一个Ref<String>,但由于我想缓存结果,所以我写了一个小Ref -wrapper:

struct CacheRef<'a> {
    borrow: Ref<'a, HashMap<u32, String>>,
    value:  &'a String,
}

这是行不通的,因为value引用borrow,所以结构不能构造。我知道引用始终有效:地图不能变异,因为Ref会锁定地图。使用原始指针而不是引用是否安全?

struct CacheRef<'a> {
    borrow: Ref<'a, HashMap<u32, String>>,
    value:  *const String,
}

我在这里忽略了什么吗?有更好(或更快(的选择吗?由于运行时开销,我试图避免RefCell

具有内部可变性的游乐场

我将用不安全的版本补充@Shepmaster安全但效率不高的答案。为此,我们将在实用程序函数中打包一些不安全的代码。

fn map_option<'a, T, F, U>(r: Ref<'a, T>, f: F) -> Option<Ref<'a, U>>
where
    F: FnOnce(&'a T) -> Option<&'a U>
{
    let stolen = r.deref() as *const T;
    let ur = f(unsafe { &*stolen }).map(|sr| sr as *const U);
    match ur {
        Some(u) => Some(Ref::map(r, |_| unsafe { &*u })),
        None => None
    }
}

我很确定这段代码是正确的。尽管编译器对生命周期相当不满意,但它们还是解决了。我们只需要注入一些原始指针来使编译器闭嘴。

有了这个,borrow的实现变得微不足道:

fn borrow<'a>(&'a self, k: u32) -> Option<Ref<'a, String>> {
    map_option(self.map.borrow(), |m| m.get(&k))
}

更新了游乐场链接

实用程序函数仅适用于Option<&T>。其他容器(如Result(将需要自己的修改副本,否则 GAT 或 HKT 才能通用实现。

我将忽略您的直接问题,以支持绝对安全的替代方案:

impl Cache {
    fn insert(&self, k: u32, v: String) {
        self.map.borrow_mut().insert(k, v);
    }
    fn borrow<'a>(&'a self, k: u32) -> Option<Ref<'a, String>> {
        let borrow = self.map.borrow();
        if borrow.contains_key(&k) {        
            Some(Ref::map(borrow, |hm| {
                hm.get(&k).unwrap()
            }))
        } else {
            None
        }
    }
}

Ref::map允许您获取Ref<'a, T>并将其转换为Ref<'a, U>。这个解决方案的丑陋部分是我们必须在哈希图中查找两次,因为我无法弄清楚如何使理想的解决方案工作:

Ref::map(borrow, |hm| {
    hm.get(&k) // Returns an `Option`, not a `&...`
})

可能需要泛型关联类型 (GAT(,即使这样,返回类型也可能是Ref<Option<T>>

正如Shepmaster所提到的,最好尽可能避免不安全。

有多种可能性:

  • Ref::map,双重查找(如牧羊人的回答所示(,
  • 具有哨兵值的Ref::map
  • 克隆返回值。

就个人而言,我会首先考虑后者。将Rc<String>存储到地图中,您的方法可以轻松返回一个完全回避问题的Option<Rc<String>>

fn get(&self, k: u32) -> Option<Rc<String>> {
    self.map.borrow().get(&k).cloned()
}

作为奖励,当您使用结果时,您的缓存不再被"锁定"。

或者,您也可以通过使用哨兵值来解决Ref::map不喜欢Option的事实:

fn borrow<'a>(&'a self, k: u32) -> Ref<'a, str> {
    let borrow = self.map.borrow();
    Ref::map(borrow, |map| map.get(&k).map(|s| &s[..]).unwrap_or(""))
}

最新更新