TL;DR: You didn't specify how to compare Dog
s and Duck
s, so the compiler is complaining at you. You have to provide a way for them to be compared.
The solution to this is way more complicated than I was expecting, and probably isn't a good idea. You should just use an enum Voiced { Dog(Dog), Duck(Duck) }
instead.
The problem
If you view the full compiler diagnostics, you will see the following:
error[E0038]: the trait `HasVoice` cannot be made into an object
--> src/lib.rs:39:36
|
39 | let mut quack_set: HashSet<Box<dyn HasVoice>> = HashSet::new();
| ^^^^^^^^^^^^ `HasVoice` cannot be made into an object
|
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
--> src/lib.rs:4:17
|
4 | trait HasVoice: PartialEq + Eq + Hash {
| -------- ^^^^^^^^^ ...because it uses `Self` as a type parameter
| |
| this trait cannot be made into an object...
You can see there's a problem with PartialEq
. But what is it?
What's the big deal with PartialEq
?
The problem is that PartialEq
is declared as PartialEq<Rhs = Self>
, which defaults to the trait PartialEq<Self>
if you don't specify a type parameter. In other words, PartialEq by default provides the comparison of a type with itself.
Consider the following code:
let duck: &dyn HasVoice = &Duck { /* ... */ };
let dog: &dyn HasVoice = &Dog { /* ... */ };
println!("{}", dog == duck);
What do you expect the outcome to be?
Well, dog == duck
is equivalent to dog.eq(duck)
, right? So Rust will look up the vtable for dog
, see that dog
does in fact have an eq
method of type fn eq(&self, rhs: Dog)
, and... wait a second. The rhs
is explicitly not a Dog
, it's a &dyn HasVoice
, which contains a pointer to the type Duck
. So there's absolutely no way you can call eq
on these two types.
This issue was discovered back in 2015, and the decided solution was to ban any Self
parameters in supertraits that aren't &self
. This means that PartialEq<Self>
cannot be used to compare two Box<dyn HasVoice>
types.
So then what?
How do I fix this?
Before we know how to fix the issue, we need to know first:
How do we compare Dog
and Duck
?
Do they compare unequal always? Equal always? Something more exotic? The compiler doesn't know, only you do, so you should specify it. What should the answer be?
Let's say that Dog
s and Duck
s are always unequal. This is probably what most people expect from dogs and ducks in real life. How do we achieve that?
Well, you can do a bunch of trait machinery manually, but the easiest way is to use the dyn_partial_eq
crate.
Here's how:
use dyn_partial_eq::*;
#[dyn_partial_eq]
trait HasVoice {
fn talk(&self);
}
#[derive(DynPartialEq, PartialEq, Eq, Hash)]
struct Duck {
name: String,
}
#[derive(DynPartialEq, PartialEq, Eq, Hash)]
struct Dog {
breed: String,
}
Then PartialEq works properly, right? So all is good?
Well, no. We forgot about Hash
.
How do we implement Hash
?
The Hash
trait is not object-safe. If you add it to the trait requirements, you'll just get a compile error. This is because Hash::hash
is a generic function, so it can't be put in a vtable.
We can get around this by making a DynHash
trait that isn't generic and instead takes a &mut dyn
reference. Here's how you might do it:
trait DynHash {
fn dyn_hash(&self, state: &mut dyn Hasher);
}
impl<H: Hash + ?Sized> DynHash for H {
fn dyn_hash(&self, mut state: &mut dyn Hasher) {
self.hash(&mut state);
}
}
#[dyn_partial_eq]
trait HasVoice: DynHash {
fn talk(&self);
}
impl Hash for dyn HasVoice {
fn hash<H: Hasher>(&self, state: &mut H) {
self.dyn_hash(state)
}
}
impl Eq for Box<dyn HasVoice> {} // required for HashSet
That's all you need.
The final code
use std::hash::{Hash, Hasher};
use std::collections::HashSet;
use dyn_partial_eq::*;
trait DynHash {
fn dyn_hash(&self, state: &mut dyn Hasher);
}
impl<H: Hash + ?Sized> DynHash for H {
fn dyn_hash(&self, mut state: &mut dyn Hasher) {
self.hash(&mut state);
}
}
#[dyn_partial_eq]
trait HasVoice: DynHash {
fn talk(&self);
}
impl Hash for Box<dyn HasVoice> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.dyn_hash(state)
}
}
impl Eq for Box<dyn HasVoice> {}
#[derive(DynPartialEq, PartialEq, Eq, Hash, Clone)]
struct Duck {
name: String,
}
#[derive(DynPartialEq, PartialEq, Eq, Hash, Clone)]
struct Dog {
breed: String,
}
impl HasVoice for Duck {
fn talk(&self) {
println!("duck quack!")
}
}
impl HasVoice for Dog {
fn talk(&self) {
println!("dog bark!")
}
}
fn main() {
let duck: Duck = Duck {
name: "an animal".to_string(),
};
let dog: Dog = Dog {
breed: "an animal".to_string(),
};
let dog2: Dog = Dog {
breed: "an animal".to_string(),
};
let mut quack_set: HashSet<Box<dyn HasVoice>> = HashSet::new();
quack_set.insert(Box::new(duck.clone()));
quack_set.insert(Box::new(dog.clone()));
quack_set.insert(Box::new(dog2));
assert_eq!(quack_set.len(), 2);
}
So that's how you implement Java in Rust. :P
For future improvement: The problem with DynHash
If you actually test the hash function we implemented here, you will notice that it doesn't work that well. More specifically, Dog { breed: "the same string".to_string() }
and Duck { name: "the same string".to_string() }
hash to exactly the same value, because Hash only cares about the stored data, not the unique type. This isn't a problem in theory, but it is quite annoying, because we really should not get hash collisions between these two types that easily. There's probably a way to fix this with a proc macro, similar to dyn_partial_eq
, but that's probably not worth doing here.