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

7

我有一个类型,它的数据存储在一个被封装在 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
    }
}

playground中有可编译的示例。

这个问题类似的问题假设内部引用始终可用,因此它们没有这个问题。

我发现了Ref::filter_map(),它可以解决这个问题,但它尚未在稳定版中提供,并且目前不清楚它离稳定还有多远。如果其他选项不可用,我将接受使用unsafe的解决方案,前提是它是安全的并依赖于文档保证。

4个回答

3
截至2022年8月11日发布的Rust 1.63版本,Ref::filter_map()已经稳定,因此可以使用以下简单的解决方案:
pub fn peek(&self, key: u32) -> Option<Ref<'_, Value>> {
    Ref::filter_map(self.storage.borrow(), |storage| storage.get(&key)).ok()
}

游乐场


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 来确保。


随后提问者提出了这个变种,为了避免链接失效,我在此将其包含在内:
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
非常优雅,谢谢。但是如果我们无论如何将v转换为指针,那么也许我们根本不需要第二个借用,只要我们分两步完成就可以了:1)从映射中检索值并将其转换为指针(因此与借用解除关联),然后调用Ref::map,其闭包将指针转换回引用:https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=8a199290db5edef6c8c917e4b8898844 - user4815162342
我猜那也行。我试图想出一个可能会出错的情况,但我想不出来。这似乎比我在这里的解决方案更糟糕。 - user3840170

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变为稳定状态,则所有这些都无关紧要。


这真的很有创意,是那种在别人指出后“显而易见”的想法。实现比我想象的稍微复杂一些,需要注释,但它是100%安全的,并完美地回答了问题,所以除非有更好的解决方案(我怀疑),否则我会接受它。在生产中,我会使用不同的方法,但对于需要返回实际引用的peek()的人来说,这是一个很好的安全解决方案,正如问题所描述的那样。 - user4815162342

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

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