How to avoid temporary allocations when using a complex key for a HashMap?
我为
这是一些代码:
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()); } |
问题在于最终的主张。 它通过了,但是需要临时堆分配,因为如果不构造
为了消除这样的临时分配,Rust提供了
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!(); } } |
这似乎是一个常见的用例,我怀疑我缺少有关
听起来像您想要这样。
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()); } |
关于生命周期参数的注释:
如果您不喜欢寿命参数,而只需要使用
您可以遵循如何用两个键实现HashMap中描述的思想。这是适用于您的案例的"借来的特征对象"答案:
创建一个可以用作常见
1 2 3 | trait Key { fn to_key(&self) -> (i32, &str); } |
为特征对象实现
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) } } |
为所有查找类型实现
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)); |
操场上的完整代码
一个重要的"陷阱"是所有主键和辅助键都必须以相同的方式散列。这意味着相同的值需要以相同的顺序和数量进入哈希计算。
您可能希望手动定义
这是另一个带有枚举的示例:
1 2 3 4 5 | #[derive(Debug, PartialEq, Eq)] enum ConfigKey { Text(String), Binary(Vec<u8>), } |
我们创建了一个仅包含引用的并行枚举,因此创建起来很轻巧。重要的是,我们定义与主要枚举相同的变体,并以相同的顺序进行排序,以便它们将散列相同。我们依赖于
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) } } |
操场上的完整代码