I'm using a complex key for HashMap
such that the key comprises two parts and one part is a String
, and I can't figure out how to do lookups via the HashMap::get
method without allocating a new String
for each lookup.
Here's some code:
#[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());
}
The problem is with the final assertion. It passes, but it requires a temporary heap allocation because I cannot construct a Complex
without constructing a String
.
To eliminate temporary allocations like this, Rust provides the Borrow
trait, which the HashMap::get
method makes use of. I understand how to make Borrow
work for simple keys. For example, the Rust Standard Library's PathBuf
implements Borrow<Path>
by making use of std::mem::transmute
under the hood, but I can't figure out how to make it work for my Complex
type:
#[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!();
}
}
This seems like a common use case, and I suspect I'm missing something important about Borrow
, but I'm at a total loss.
It sounds like you want this.
Cow
will accept a&str
orString
.A comment about lifetime parameters:
If you don't like the lifetime parameter and you only need to work with
&'static str
orString
then you can useCow<'static, str>
and remove the other lifetime parameters from the impl block and struct definition.You can follow the ideas described in How to implement HashMap with two keys?. Here's the "borrowed trait object" answer applied to your case:
Create a trait that we can use as a common
Borrow
target:Implement the
HashMap
-required traits for the trait object:Implement the trait for our primary type and any secondary lookup types:
Implement
Borrow
for all the lookup types as returning our trait object:Convert to the trait object at query time:
The complete code in the playground
One important "gotcha" is that all of your primary key and your secondary keys must hash in the same manner. This means that the same values need to go into the hash computation in the same order and amount.
You may wish to define
Hash
by hand to ensure that your primary and secondary keys hash the same!Here's another example, this time with an enum:
We create a parallel enum that is composed of only references, so it's lightweight to create. It's important that we define the same variants and in the same order as the primary enum so they will hash the same. We rely on the fact that
String
and&str
hash using the same algorithm, as doVec<T>
and&[T]
:We use this new enum as our common underlying key type:
And implement our trait for our primary and secondary keys:
The complete code in the playground