【问题标题】:How to return an *optional* reference into RefCell contents如何将*可选*引用返回到 RefCell 内容
【发布时间】:2021-04-24 12:53:22
【问题描述】:

我有一个将其数据存储在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&lt;Ref&lt;'_, Value&gt;&gt;。这是一个问题,因为Ref::map 要求您返回对存在于RefCell 中的东西的引用,所以即使我想返回Ref&lt;'_, Option&lt;Value&gt;&gt;,它也不起作用,因为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 的解决方案,前提是它是可靠的并且依赖于书面保证。

【问题讨论】:

    标签: rust refcell


    【解决方案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 的任何定义的最通用的方法,而不是最简单的方法。 (这是可能的,因为 &amp;'static Value 满足 Ref 的要求 - 引用只需要足够长的时间,而不是实际指向 RefCell 的内容。)

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

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

    【讨论】:

    • 这真是太有创意了,这种想法一旦被别人指出就“显而易见”。实现比我想要的稍微复杂一些,并且需要 cmets,但它是 100% 安全的并且完美地回答了这个问题,所以我会接受它,除非出现更好的东西(我对此表示怀疑)。在生产中,我将使用different approach,但对于需要返回实际参考的peek() 的人来说,这是一个很好的安全解决方案,如问题中所述。
    【解决方案2】:

    我想出了这个:

    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 _)))
        }
    }
    

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

    认为这是正确的。尽管如此,我还是会继续期待 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 }))
    }
    

    【讨论】:

    • 这很优雅,谢谢。但是如果我们无论如何将v转换为指针,那么也许我们根本不需要第二次借用,我们可以用第一个做任何事情,只要我们分两步完成:1)从map中检索值并将其转换为指针(从而将其与借用解除关联),然后调用Ref::map,其闭包将指针转换回引用:play.rust-lang.org/…
    • 我想这也有效。我试图想出一个它可能会崩溃的情况,但我想不出一个。这似乎并不比我这里的解决方案差。
    【解决方案3】:

    这是我最终使用的解决方案,直到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();
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-25
      • 1970-01-01
      • 2015-07-28
      • 2021-04-27
      • 1970-01-01
      • 2019-06-11
      相关资源
      最近更新 更多