【问题标题】:Is it safe to use a raw pointer to access the &T of a RefCell<HashMap<T>>?使用原始指针访问 RefCell<HashMap<T>> 的 &T 是否安全?
【发布时间】:2018-03-19 13:33:37
【问题描述】:

我有一个类似缓存的结构,内部使用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 方法很简单,但我遇到了借用功能的问题。我可以返回一个Ref&lt;String&gt;,但是因为我想缓存结果,所以我写了一个小的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

Playground with internal mutability

【问题讨论】:

  • 我写了一个小的Ref-wrapper——写这样一个包装器究竟有什么好处?
  • 使用包装器,我可以在地图中搜索键并存储结果。取消引用Ref 将产生&amp;HashMap,取消引用包装器将产生&amp;T
  • Ref 有一个map 关联函数,应该用于将对整个地图的引用转换为对一个元素的引用。但是它的签名不够灵活;我无法使用 Option 包装器。
  • @SebastianRedl 是的,I'm stuck as well.

标签: pointers rust lifetime unsafe


【解决方案1】:

我将用不安全的版本补充@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&lt;&amp;T&gt;。其他容器(例如 Result)需要自己修改后的副本,否则 GAT 或 HKT 才能通用实现。

【讨论】:

  • 我认为这最适合我的需求。我更新了Playground 并添加了一个可变版本。
【解决方案2】:

我将忽略您的直接问题,转而选择一个绝对安全的替代方案:

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&lt;'a, T&gt; 并将其转换为Ref&lt;'a, U&gt;。这个解决方案的丑陋部分是我们必须在 hashmap 中查找两次,因为我不知道如何使理想的解决方案起作用:

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

可能需要通用关联类型 (GAT),即使这样,返回类型也可能是 Ref&lt;Option&lt;T&gt;&gt;

【讨论】:

    【解决方案3】:

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

    有多种可能性:

    • Ref::map,双重查找(如 Shepmaster 的回答所示),
    • Ref::map 带有标记值,
    • 克隆返回值。

    就个人而言,我会先考虑后者。将Rc&lt;String&gt; 存储到您的地图中,您的方法可以轻松返回Option&lt;Rc&lt;String&gt;&gt;,这完全回避了这些问题:

    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(""))
    }
    

    【讨论】:

      猜你喜欢
      • 2019-08-14
      • 2016-11-23
      • 1970-01-01
      • 2019-09-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-01-20
      • 2019-12-13
      相关资源
      最近更新 更多