关于rust:在对HashMap使用复杂密钥时,如何避免临时分配?

How to avoid temporary allocations when using a complex key for a HashMap?

我为HashMap使用了一个复杂的键,因此该键包括两部分,一个部分是String,并且我无法弄清楚如何通过HashMap::get方法进行查找而不分配新的进行每次查找。

这是一些代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[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,但是我不知道如何使它适用于我的Complex类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#[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的重要信息,但我全神贯注。


听起来像您想要这样。

Cow将接受&strString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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());
}

关于生命周期参数的注释:

如果您不喜欢寿命参数,而只需要使用&'static strString,则可以使用Cow<'static, str>并从impl块和结构定义中删除其他寿命参数。


您可以遵循如何用两个键实现HashMap中描述的思想。这是适用于您的案例的"借来的特征对象"答案:

创建一个可以用作常见Borrow目标的特征:

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

为特征对象实现HashMap必需的特征:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 + '_ {}

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

1
2
3
4
5
6
7
8
9
10
11
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,以返回我们的特征对象:

1
2
3
4
5
6
7
8
9
10
11
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
    }
}

在查询时转换为特征对象:

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

操场上的完整代码

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

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

这是另一个带有枚举的示例:

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

我们创建了一个仅包含引用的并行枚举,因此创建起来很轻巧。重要的是,我们定义与主要枚举相同的变体,并以相同的顺序进行排序,以便它们将散列相同。我们依赖于String&str使用相同算法进行散列的事实,与Vec< T >&[T]一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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]),
}

我们使用这个新的枚举作为我们常见的基础键类型:

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

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

1
2
3
4
5
6
7
8
9
10
11
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)
    }
}

操场上的完整代码