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

Is it safe to use a raw pointer to access the &T of a RefCell<HashMap<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)
    }
}

Playground with external mutability

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

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

这将不起作用,因为 value 引用了 borrow,因此无法构造该结构。我知道引用始终有效:地图无法更改,因为 Ref locks 地图。使用原始指针而不是引用安全吗?

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

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

Playground with internal mutability

我将忽略您的直接问题,转而使用绝对安全的替代方法:

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>。这个解决方案的丑陋部分是我们必须在 hashmap 中查找两次,因为我不知道如何使理想的解决方案起作用:

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

可能 需要通用关联类型 (GAT),即使这样 return 类型也可能是 Ref<Option<T>>.

我将用不安全的版本补充@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))
}

Updated playground link

效用函数仅适用于Option<&T>。其他容器(例如 Result)将需要它们自己的修改副本,或者 GAT 或 HKT 以通用实现。

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

有多种可能性:

  • Ref::map,双 look-up(如 Shepmaster 的回答所示),
  • Ref::map 具有标记值,
  • 克隆 return 值。

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

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

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

或者,您可以 work-around 通过使用标记值 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(""))
}