【问题标题】:How to avoid temporary allocations when using a complex key for a HashMap?HashMap 使用复杂键时如何避免临时分配?
【发布时间】:2016-04-07 15:33:13
【问题描述】:

我为HashMap 使用了一个复杂的密钥,这样该密钥包含两部分,一部分是String,我无法弄清楚如何通过HashMap::get 方法进行查找而不分配每次查找都使用新的String

这里有一些代码:

#[derive(Debug, Eq, Hash, PartialEq)]
struct Complex {
    n: i32,
    s: String,
}

impl Complex {
    fn new<S: Into<String>>(n: i32, s: S) -> Self {
        Complex { n: n, s: s.into() }
    }
}

fn main() {
    let mut m = std::collections::HashMap::<Complex, i32>::new();
    m.insert(Complex::new(42, "foo"), 123);

    // OK, but allocates temporary String
    assert_eq!(123, *m.get(&Complex::new(42, "foo")).unwrap());
}

问题在于最终断言。它通过了,但它需要临时堆分配,因为如果不构造 String,我就无法构造 Complex

为了消除这样的临时分配,Rust 提供了 Borrow 特征,HashMap::get 方法利用了它。我了解如何使 Borrow 用于简单的键。例如,Rust 标准库的 PathBuf 通过使用 std::mem::transmute 在底层实现 Borrow&lt;Path&gt;,但我不知道如何使它适用于我的 Complex 类型:

#[derive(Debug)]
struct Borrowable {
    // ??? -- What goes here? Perhaps something like:
    n: i32,
    s1: &str, // ??? -- But what would the lifetime be? Or maybe:
    s2: str,  // ??? -- But how would I extend this to a complex type
              //        containing two or more strings?
}

impl Borrowable {
    fn new(n: i32, s: &str) -> &Self {
         // ??? -- What goes here? It must not allocate.
        unimplemented!();
    }
}

impl std::borrow::Borrow<Borrowable> for Complex {
    fn borrow(&self) -> &Borrowable {
        // ??? -- What goes here? How can I transmute a Complex into a
        //        &Borrowable?
        unimplemented!();
    }
}

这似乎是一个常见的用例,我怀疑我遗漏了一些关于 Borrow 的重要信息,但我完全不知所措。

【问题讨论】:

    标签: rust


    【解决方案1】:

    听起来你想要这个。

    Cow 将接受 &amp;strString

    use std::borrow::Cow;
    
    #[derive(Debug, Eq, Hash, PartialEq)]
    struct Complex<'a> {
        n: i32,
        s: Cow<'a, str>,
    }
    
    impl<'a> Complex<'a> {
        fn new<S: Into<Cow<'a, str>>>(n: i32, s: S) -> Self {
            Complex { n: n, s: s.into() }
        }
    }
    
    fn main() {
        let mut m = std::collections::HashMap::<Complex<'_>, i32>::new();
        m.insert(Complex::new(42, "foo"), 123);
    
        assert_eq!(123, *m.get(&Complex::new(42, "foo")).unwrap());
    }
    

    关于生命周期参数的评论:

    如果您不喜欢生命周期参数并且只需要使用&amp;'static strString,那么您可以使用Cow&lt;'static, str&gt; 并从 impl 块和结构定义中删除其他生命周期参数。

    【讨论】:

    • 我需要一两天来消化这个。这很简单,但它让我大吃一惊。我主要担心的是,在我的特定情况下,Complex 暴露在我的 crate 的 API 中,所以我需要确保我可以用生命周期参数给类型增加负担,而不会过多地混淆我的界面。我最初的反应是Cow 的写时复制功能太重了,因为我从不做任何复制,但在某种意义上我几乎是,因为有时我使用实例,有时我使用引用.在我消化完你的答案后,我会告诉你它是如何工作的。
    • 您也许可以摆脱生命周期参数。查看我的编辑。
    • 我无法消除生命周期参数,因为我有时会从非静态 str 借用。尽管如此,我真的很喜欢你的想法,因为它会简化其他地方的代码,因为新的Complex 涵盖了拥有和借用案例——不需要为每个案例使用唯一类型。让我感兴趣的一件事是,Rust 标准库没有走使用Cow 的路线,而是使用两种类型来涵盖Path/PathBuf 等人的拥有和借用案例。 Cow 的想法似乎更普遍,因为它也适用于复杂类型。这样做是为了提高运行时效率吗?
    【解决方案2】:

    您可以按照How to implement HashMap with two keys? 中描述的想法进行操作。这是应用于您的案例的"borrowed trait object" answer

    创建一个我们可以用作通用Borrow 目标的特征:

    trait Key {
        fn to_key(&self) -> (i32, &str);
    }
    

    为特征对象实现HashMap-required 特征:

    use std::hash::{Hash, Hasher};
    
    impl Hash for dyn Key + '_ {
        fn hash<H: Hasher>(&self, state: &mut H) {
            self.to_key().hash(state)
        }
    }
    
    impl PartialEq for dyn Key + '_ {
        fn eq(&self, other: &Self) -> bool {
            self.to_key() == other.to_key()
        }
    }
    
    impl Eq for dyn Key + '_ {}
    

    为我们的主要类型和任何次要查找类型实现特征:

    impl Key for Complex {
        fn to_key(&self) -> (i32, &str) {
            (self.n, &self.s)
        }
    }
    
    impl<'a> Key for (i32, &'a str) {
        fn to_key(&self) -> (i32, &str) {
            (self.0, self.1)
        }
    }
    

    为所有查找类型实现Borrow,以返回我们的特征对象:

    impl<'a> Borrow<dyn Key + 'a> for Complex {
        fn borrow(&self) -> &(dyn Key + 'a) {
            self
        }
    }
    
    impl<'a> Borrow<dyn Key + 'a> for (i32, &'a str) {
        fn borrow(&self) -> &(dyn Key + 'a) {
            self
        }
    }
    

    在查询时转换为 trait 对象:

    assert_eq!(Some(&123), m.get((42, "foo").borrow() as &dyn Key));
    

    The complete code in the playground


    一个重要的“陷阱”是您的所有主键和辅助键必须以相同的方式散列。这意味着相同的值需要以相同的顺序和数量进入哈希计算。

    您可能希望手动定义Hash,以确保您的主键和辅助键哈希相同!

    这是另一个例子,这次是枚举:

    #[derive(Debug, PartialEq, Eq)]
    enum ConfigKey {
        Text(String),
        Binary(Vec<u8>),
    }
    

    我们创建了一个仅由引用组成的并行枚举,因此创建起来很轻量级。重要的是我们定义相同的变体并以与主枚举相同的顺序,以便它们将散列相同。我们依赖于 String&amp;str 使用相同算法散列的事实,Vec&lt;T&gt;&amp;[T] 也是如此:

    impl ConfigKey {
        fn as_ref(&self) -> ConfigKeyRef<'_> {
            match self {
                ConfigKey::Text(t) => ConfigKeyRef::Text(t),
                ConfigKey::Binary(b) => ConfigKeyRef::Binary(b),
            }
        }
    }
    
    #[derive(Hash, PartialEq, Eq)]
    enum ConfigKeyRef<'a> {
        Text(&'a str),
        Binary(&'a [u8]),
    }
    

    我们使用这个新的枚举作为我们共同的底层键类型:

    trait Key {
        fn to_key(&self) -> ConfigKeyRef<'_>;
    }
    

    并为我们的主键和辅助键实现我们的特征:

    impl Key for ConfigKey {
        fn to_key(&self) -> ConfigKeyRef<'_> {
            self.as_ref()
        }
    }
    
    impl<'a> Key for &'a str {
        fn to_key(&self) -> ConfigKeyRef<'_> {
            ConfigKeyRef::Text(self)
        }
    }
    

    The complete code in the playground

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多