问题
I want to be able to use a variety of different types as keys in a HashMap
, all of which would implement Hash
. This seems like it should be possible: from reading the docs it seems that every Hasher
will produce a u64
result, so they eventually get reduced down to a common type. Effectively I want to do:
use std::{collections::HashMap, hash::Hash};
fn x(_: HashMap<Box<dyn Hash>, ()>) {}
which I'm not allowed to do:
error[E0038]: the trait `std::hash::Hash` cannot be made into an object
--> src/lib.rs:3:9
|
3 | fn x(_: HashMap<Box<dyn Hash>, ()>) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::hash::Hash` cannot be made into an object
It seems like I could create a Hasher
(e.g. RandomState
), use that to manually calculate hash values, then store the u64
result in a HashMap<u64, _>
but that seems kind of overly complex. I don't ever want to get the key values back out again, I just need to be able to compare the hash values. Is there a HashMap
alternative I'm not aware of? Or am I looking at this in a totally wrong way?
回答1:
It seems like I could create a
Hasher
(e.g.RandomState
), use that to manually calculate hash values, then store theu64
result in aHashMap<u64, _>
but that seems kind of overly complex.
Unfortunately that's overly simple - since a hash function discards some information, hash tables don't just work on hashes, they also need the original key to compare it with the key you're inserting or looking for. This is necessary to handle the case when two keys produce the same hash, which is both possible and allowed.
Having said that, you can implement Python-style hash tables with heterogeneous keys in Rust, but you'll need to box the keys, i.e. use Box<dyn Key>
as the type-erased key, with Key
being a trait you define for all concrete types you want to use as the hash key. In particular, you'll need to:
- define the
Key
trait that specifies how to compare and hash the actual keys; - (optionally) provide a blanket implementation of that trait for types that are themselves
Hash
andEq
so the user doesn't need to do that for every type manually; - define
Eq
andHash
forBox<dyn Key>
using the methods the trait provides, makingBox<dyn Key>
usable as key instd::collections::HashMap
.
The Key
trait could be defined like this:
trait Key {
fn eq(&self, other: &dyn Key) -> bool;
fn hash(&self) -> u64;
// see https://stackoverflow.com/a/33687996/1600898
fn as_any(&self) -> &dyn Any;
}
And here is a blanket implementation of Key
for any type that is Eq
and Hash
:
use std::any::{Any, TypeId};
use std::collections::{HashMap, hash_map::DefaultHasher};
use std::hash::{Hash, Hasher};
impl<T: Eq + Hash + 'static> Key for T {
fn eq(&self, other: &dyn Key) -> bool {
if let Some(other) = other.as_any().downcast_ref::<T>() {
return self == other;
}
false
}
fn hash(&self) -> u64 {
let mut h = DefaultHasher::new();
// mix the typeid of T into the hash to make distinct types
// provide distinct hashes
Hash::hash(&(TypeId::of::<T>(), self), &mut h);
h.finish()
}
fn as_any(&self) -> &dyn Any {
self
}
}
Finally, these impls will make Box<dyn Key>
usable as hash table keys:
impl PartialEq for Box<dyn Key> {
fn eq(&self, other: &Self) -> bool {
Key::eq(self.as_ref(), other.as_ref())
}
}
impl Eq for Box<dyn Key> {}
impl Hash for Box<dyn Key> {
fn hash<H: Hasher>(&self, state: &mut H) {
let key_hash = Key::hash(self.as_ref());
state.write_u64(key_hash);
}
}
// just a convenience function to box the key
fn into_key(key: impl Eq + Hash + 'static) -> Box<dyn Key> {
Box::new(key)
}
With all that in place, you can use the keys almost as you would in a dynamic language:
fn main() {
let mut map = HashMap::new();
map.insert(into_key(1u32), 10);
map.insert(into_key("foo"), 20);
map.insert(into_key("bar".to_string()), 30);
assert_eq!(map.get(&into_key(1u32)), Some(&10));
assert_eq!(map.get(&into_key("foo")), Some(&20));
assert_eq!(map.get(&into_key("bar".to_string())), Some(&30));
}
Playground
Note that this implementation will always consider values of distinct concrete types to have different values. While Python's dictionaries will consider keys 1
and 1.0
to be the same key (while "1"
will be distinct), an into_key(1u32)
will be distinct not just from into_key(1.0)
, but also from into_key(1u64)
. Likewise, into_key("foo")
will be a distinct from into_key("foo".to_string())
. This could be changed by manually implementing Key
for the types you care about, in which case the blanket implementation must be removed.
回答2:
I could not understand if you need to compare 2 hash or insert a hash into a HashMap, so i'm going to give you aswears to both.
This generic function generate hash for any given type.
fn hash<T>(obj: T) -> u64
where
T: Hash,
{
let mut hasher = DefaultHasher::new();
obj.hash(&mut hasher);
hasher.finish()
}
To compare you would do something like this :
let string = String::from("hello");
let _i32 = 12;
let result = hash(&string) == hash(_i32);
println!("{} == {} -> {}", &string, _i32, result);
This also work for struct :
#[derive(Hash, Debug)]
struct Person{
name : String,
age : i32
}
let person1 = Person{name : "Kelvin".to_string(), age: 19};
let person2 = Person{name : "Dmitri".to_string(), age: 17};
let result = hash(&person1) == hash(&person2);
println!("{:?} == {:?} -> {}", &person1, &person2, result);
To insert Hash into a HashMap :
let mut HashMap : HashMap<u64, ()> = HashMap::new();
HashMap.insert(hash("hello world"), ());
来源:https://stackoverflow.com/questions/64838355/how-do-i-create-a-hashmap-with-type-erased-keys