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
It sounds like you want this.
Cow
will accept a &str
or String
.
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());
}
A comment about lifetime parameters:
If you don't like the lifetime parameter and you only need to work with &'static str
or String
then you can use Cow<'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:
trait Key {
fn to_key(&self) -> (i32, &str);
}
Implement the HashMap
-required traits for the trait object:
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 + '_ {}
Implement the trait for our primary type and any secondary lookup types:
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)
}
}
Implement Borrow
for all the lookup types as returning our trait object:
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
}
}
Convert to the trait object at query time:
assert_eq!(Some(&123), m.get((42, "foo").borrow() as &dyn Key));
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:
#[derive(Debug, PartialEq, Eq)]
enum ConfigKey {
Text(String),
Binary(Vec<u8>),
}
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 do Vec<T>
and &[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]),
}
We use this new enum as our common underlying key type:
trait Key {
fn to_key(&self) -> ConfigKeyRef<'_>;
}
And implement our trait for our primary and secondary keys:
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