如何将*可选* 引用返回到 RefCell 内容中

时间:2021-04-24 12:53:22

标签: rust refcell

我有一个类型将其数据存储在 Rc<RefCell<>> 后面的容器中,该容器大部分对公共 API 隐藏。例如:

struct Value;

struct Container {
    storage: Rc<RefCell<HashMap<u32, Value>>>,
}

impl Container {
    fn insert(&mut self, key: u32, value: Value) {
        self.storage.borrow_mut().insert(key, value);
    }

    fn remove(&mut self, key: u32) -> Option<Value> {
        self.storage.borrow_mut().remove(&key)
    }

    // ...
}

但是,查看容器内部需要返回一个 Ref。这可以使用 Ref::map() 来实现 - 例如:

// peek value under key, panicking if not present
fn peek_assert(&self, key: u32) -> Ref<'_, Value> {
    Ref::map(self.storage.borrow(), |storage| storage.get(&key).unwrap())
}

但是,我想要一个非恐慌版本的 peek,它会返回 Option<Ref<'_, Value>>。这是一个问题,因为 Ref::map 要求您返回对存在于 RefCell 中的内容的引用,因此即使我想返回 Ref<'_, Option<Value>>,它也不起作用,因为该选项返回storage.get() 是短暂的。

尝试使用 Ref::map 从先前查找的键创建 Ref 也不会编译:

// doesn't compile apparently the borrow checker doesn't understand that `v`
// won't outlive `_storage`.
fn peek(&self, key: u32) -> Option<Ref<'_, Value>> {
    let storage = self.storage.borrow();
    if let Some(v) = storage.get(&key) {
        Some(Ref::map(storage, |_storage| v))
    } else {
        None
    }
}

有效的方法是执行两次查找,但这是我非常想避免的:

// works, but does lookup 2x
fn peek(&self, key: u32) -> Option<Ref<'_, Value>> {
    if self.storage.borrow().get(&key).is_some() {
        Some(Ref::map(self.storage.borrow(), |storage| {
            storage.get(&key).unwrap()
        }))
    } else {
        None
    }
}

可编译示例 in the playground

诸如 this one 之类的相关问题假设内部引用始终可用,因此它们不存在该问题。

我发现 Ref::filter_map()solve this,但它尚未在稳定版上可用,目前尚不清楚它离稳定版还有多远。除非有其他选择,否则我会接受使用 unsafe 的解决方案,前提是它是合理的并且依赖于文档化的保证。

3 个答案:

答案 0 :(得分:1)

我设法想出了这个:

fn peek<'a>(&'a self, key: u32) -> Option<Ref<'a, Value>> {
    // Safety: we perform a guarded borrow, then an unguarded one.
    // If the former is successful, so must be the latter.
    // Conceptually, they are the same borrow: we just take the pointer
    // from one and the dynamic lifetime guard from the other.
    unsafe {
        let s = self.storage.borrow();
        let u = self.storage.try_borrow_unguarded().unwrap();
        u.get(&key).map(|v| Ref::map(s, |_| &*(v as *const _)))
    }
}

我只是简单地借用哈希映射两次,然后将生命周期丢弃(通过将引用转换为指针),然后通过重新借用指针的所指对象将其带回来。我取消了生命周期参数,以确保它不会变得太长。

认为是对的。尽管如此,我还是会继续期待 filter_map 只是为了确定。


Asker 后来想出了这个变体,为了避免链接腐烂,我将其包含在此处:

fn peek<'a>(&'a self, key: u32) -> Option<Ref<'a, Value>> {
    // Safety: we convert the reference obtained from the guarded borrow
    // into a pointer. Dropping the reference allows us to consume the
    // original borrow guard and turn it into a new one (with the same
    // lifetime) that refers to the value inside the hashmap.
    let s = self.storage.borrow();
    s.get(&key)
        .map(|v| v as *const _)
        .map(|v| Ref::map(s, |_| unsafe { &*v }))
}

答案 1 :(得分:1)

您可以使用副作用来传达查找是否成功,如果没有成功值,则从 Ref::map 返回任意值。

impl Container {
    // ...

    fn peek(&self, key: u32) -> Option<Ref<'_, Value>> {
        let storage = self.storage.borrow();
        if storage.is_empty() {
            // The trick below requires the map to be nonempty, but if it's
            // empty, then we don't need to do a lookup.
            return None;
        }

        // Find either the correct value or an arbitrary one, and use a mutable
        // side channel to remember which one it is.
        let mut failed = false;
        let ref_maybe_bogus: Ref<'_, Value> = Ref::map(storage, |storage| {
            storage.get(&key).unwrap_or_else(|| {
                // Report that the lookup failed.
                failed = true;
                // Return an arbitrary Value which will be ignored.
                // The is_empty() check above ensured that one will exist.
                storage.values().next().unwrap()
            })
        });
        
        // Return the ref only if it's due to a successful lookup.
        if failed {
            None
        } else {
            Some(ref_maybe_bogus)
        }
    }
}

改进:

  • 如果 Value 类型可以有常量实例,那么你可以返回其中之一而不是要求映射非空;上面的方法只是适用于 Value 的任何定义的最通用的方法,而不是最简单的方法。 (这是可能的,因为 &'static Value 满足 Ref 的要求 - 引用只需要存活足够长的时间,而不是实际指向 RefCell 的内容。)

  • 如果 Value 类型可以有一个常量实例它与地图中可以找到的任何有意义的实例不同(“哨兵值”),那么您可以在最终的 if 中检查该值,而不是检查单独的布尔变量。然而,这并没有特别简化代码;如果您有一个哨兵用于其他目的,或者如果您喜欢避免副作用的“纯功能”编码风格,它最有用。

当然,如果 Ref::filter_map 变得稳定,这一切都没有实际意义。

答案 2 :(得分:0)

这是我在 Ref::filter_map() 稳定之前最终使用的解决方案。它改变了问题中指定的 peek() 的签名,所以我不会接受这个答案,但它可能对偶然发现这个问题的其他人有用。

虽然 peek() 是一个强大的原语,但它在调用站点的使用归结为检查值的某些属性并基于此做出决策。对于这种用法,调用者不需要保留引用,它只需要临时访问它来提取它关心的属性。所以我们可以让 peek 接受一个检查值的闭包,并返回它的结果:

fn peek<F: FnOnce(&Value) -> R, R>(&self, key: u32, examine: F) -> Option<R> {
    self.storage.borrow().get(&key).map(examine)
}

最初指定的 peek() 在哪里写:

if let Some(event) = container.peek() {
    if event.time() >= deadline {
        container.remove_next();
    }
}

...有了这个答案中的 peek(),人们会写成:

if let Some(event_time) = container.peek(|e| e.time()) {
    if event_time >= deadline {
        container.remove_next();
    }
}
相关问题