【问题标题】:Entry::or_insert executes despite a value already existing尽管值已经存在,但 Entry::or_insert 仍然执行
【发布时间】:2020-02-07 08:41:44
【问题描述】:

在chapter 13 of the Rust book 中,您实现了一个Cacher 结构用于延迟初始化,以演示闭包和函数式编程的使用。作为练习,他们鼓励读者尝试创建一个通用的Cacher,它可以存储多个值。为此,他们建议使用Hashmap。

尝试修改缓存器以保存哈希映射而不是单个值。哈希映射的键将是传入的 arg 值,哈希映射的值将是对该键调用闭包的结果。 value 函数不会直接查看 self.value 是否具有 Some 或 None 值,而是会在 hash map 中查找 arg 并返回该值(如果存在)。如果不存在,缓存器将调用闭包并将结果值保存在与其 arg 值关联的哈希映射中。

当前 Cacher 实现的第二个问题是它只接受带有一个 u32 类型参数并返回 u32 的闭包。例如,我们可能希望缓存采用字符串切片并返回 usize 值的闭包的结果。要解决此问题,请尝试引入更多通用参数以增加缓存功能的灵活性。

为了解决这个练习,我使用了以下代码:

struct Cacher<T, K, V>
    where T: Fn(K) -> V
{
    calculation: T,
    values: HashMap<K, V>,
}

impl<T, K, V> Cacher<T, K, V>
    where T: Fn(K) -> V,
          K: std::cmp::Eq + std::hash::Hash + Clone,
{
    fn new(calculation: T) -> Cacher<T, K, V> {
        Cacher {
            calculation,
            values: HashMap::new(),
        }
    }

    fn value(&mut self, intensity: K) -> &V {
        self.values.entry(intensity.clone()).or_insert((self.calculation)(intensity))
    }
}

这段代码可以编译并运行,但由于(self.calculation)(intensity) 总是被执行,所以它不能作为正确的Cacher。即使条目存在。我从文档和示例中了解到,Entry::or_insert 函数仅在Entry 不存在时才会执行。

我知道问题 Is it possible to use a single generic for both key and value of a HashMap? 中练习的解决方案,但我想知道是否可以按照我目前的方式解决问题。

编辑:如评论中所述:or_insert_with 不能解决问题。尝试or_insert_with(|| (self.calculation)(intensity.clone())) 时出现以下错误error[E0502]: cannot borrow self as immutable because it is also borrowed as mutable。

【问题讨论】:

  • TL;DR:你试过or_insert_with吗?
  • 我刚刚尝试了or_insert_with(|| (self.calculation)(intensity.clone())) 并收到以下错误:error[E0502]: cannot borrow `self` as immutable because it is also borrowed as mutable。 or_insert 和 or_insert_with 除了前者期望一个值而后者期望一个返回值的闭包之外,还有什么区别吗?
  • 哼,没错,你不能这样。
  • 您只需要将对calculation 的引用存储在前一行的单独let 语句中:let calculation = &amp;self.calculation;。借用检查器足够聪明,可以看到 self.calculation 和 self.values 在同一范围内发生时不会重叠,但如果您只在闭包内使用 self.calculation,它将触发对所有 @987654345 的借用@,确实与self.values重叠。
  • @SvenMarnach 非常感谢。解决方案是添加您提到的行并使用@justinas 建议使用or_insert_with。

标签: rust


【解决方案1】:

您的代码的问题是函数参数总是在调用 Rust(和大多数命令式语言)中的函数之前进行评估。这意味着在调用or_insert() 之前,代码将无条件调用(self.calculation)(intensity)。 or_insert() 函数将在内部检查一个值是否已经存在于条目中,如果没有,则仅插入作为参数传递的新值,但这仅发生在 之后 self.calculation 是已经调用了。

这个问题可以通过or_insert_with()方法解决。此方法接受闭包而不是值,并且仅在需要插入值时才调用闭包。完整代码如下:

use std::collections::HashMap;

struct Cacher<T, K, V> {
    calculation: T,
    values: HashMap<K, V>,
}

impl<T, K, V> Cacher<T, K, V>
where
    K: std::cmp::Eq + std::hash::Hash + Clone,
{
    fn new(calculation: T) -> Cacher<T, K, V> {
        Cacher {
            calculation,
            values: HashMap::new(),
        }
    }

    fn value(&mut self, intensity: K) -> &V
    where
        T: Fn(K) -> V,
    {
        let calculation = &self.calculation;
        self.values
            .entry(intensity.clone())
            .or_insert_with(|| calculation(intensity))
    }
}

value() 实现的一个微妙之处是您需要将对self.calculation 的引用存储在一个单独的变量中。否则,关闭将触发self 的借用,这与self.values 的调用self.values.entry() 触发的可变借用重叠。如果您在外部范围内仅显式借用 self.calculation,则借用检查器足够聪明,可以确定它与 self.values 不重叠。

作为旁注,我建议使用rustfmt 来实现一致的代码格式。我还建议尽可能缩小特征范围,以避免不必要的重复。这两个建议都包含在上面的代码中。

【讨论】:

  • 感谢您的解释;我有同样的问题。使用or_insert_with() 比简单的match 有什么优势,除了更简洁和更清晰的代码吗?我在使用 match 时遇到了同样的问题,类似于他们最初在书中的做法。
  • @iDuran 这个版本使用entry() 接口只计算一次key的哈希值和它在map中的位置。实际执行插入时不需要重新计算它们。你的匹配代码比那更好或更差取决于你的代码,你没有分享,所以我不知道。
猜你喜欢
  • 2022-12-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-04-08
  • 1970-01-01
  • 2017-04-17
  • 2023-01-10
  • 2017-04-28
相关资源
最近更新 更多