I was working on hashing a set of structs that all implement the same trait and some are zero-sized and some not. I have a sequence of them and am using the hash to represent the cumulative set of them up to that point in the sequence (for caching type uses mostly). I was then very surprised when I found that that hashing a ZST is actually a no-op. Here’s a minimal example:
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
#[derive(Hash)]
struct Empty {
}
fn main() {
let mut s = DefaultHasher::new();
1.hash(&mut s);
println!("Hash is {}", s.finish());
let e = Empty{};
e.hash(&mut s);
println!("Hash is {}", s.finish());
}
It returns the surprising result:
Hash is 1742378985846435984
Hash is 1742378985846435984
I’d say this breaks the assumption of what .hash() does in that you’ve passed a new value into it and the hash hasn’t changed. It’s definitely a corner case though and having it be a no-op should definitely help performance and be fine in almost all cases.
For my specific use case this was quite easy to work around. My trait includes a .name() that returns a string so I hashed that first before hashing the struct. But I thought it was worth it to discuss first