How can I have a collection of objects that differ by their associated type?
I eventually found a way to do it that I'm happy with. Instead of having a vector of Box<Check<???>>
objects, have a vector of closures that all have the same type, abstracting away the very functions that get called:
fn main() {
type Probe = Box<Fn(i32) -> Option<Box<Error>>>;
let numbers: Vec<i32> = vec![ 1, -4, 64, -25 ];
let checks = vec![
Box::new(|num| EvenCheck.check_number(num).map(|u| Box::new(u) as Box<Error>)) as Probe,
Box::new(|num| NegativeCheck.check_number(num).map(|u| Box::new(u) as Box<Error>)) as Probe,
];
for number in numbers {
for check in checks.iter() {
if let Some(error) = check(number) {
println!("{}", error.description());
}
}
}
}
Not only does this allow for a vector of Box<Error>
objects to be returned, it allows the Check
objects to provide their own Error associated type which doesn't need to implement PartialEq
. The multiple as
es look a little messy, but on the whole it's not that bad.
When you write an impl Check
and specialize your type Error
with a concrete type, you are ending up with different types.
In other words, Check<Error = NegativeError>
and Check<Error = EvenError>
are statically different types. Although you might expect Check<Error>
to describe both, note that in Rust NegativeError
and EvenError
are not sub-types of Error
. They are guaranteed to implement all methods defined by the Error
trait, but then calls to those methods will be statically dispatched to physically different functions that the compiler creates (each will have a version for NegativeError
, one for EvenError
).
Therefore, you can't put them in the same Vec
, even boxed (as you discovered). It's not so much a matter of knowing how much space to allocate, it's that Vec
requires its types to be homogeneous (you can't have a vec![1u8, 'a']
either, although a char
is representable as a u8
in memory).
Rust's way to "erase" some of the type information and gain the dynamic dispatch part of subtyping is, as you discovered, trait objects.
If you want to give another try to the trait object approach, you might find it more appealing with a few tweaks...
You might find it much easier if you used the
Error
trait instd::error
instead of your own version of it.You may need to
impl Display
to create a description with a dynamically builtString
, like so:impl fmt::Display for EvenError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} is even", self.number) } } impl Error for EvenError { fn description(&self) -> &str { "even error" } }
Now you can drop the associated type and have
Check
return a trait object:trait Check { fn check_number(&self, number: i32) -> Option<Box<Error>>; }
your
Vec
now has an expressible type:let mut checks: Vec<Box<Check>> = vec![ Box::new(EvenCheck) , Box::new(NegativeCheck) , ];
The best part of using
std::error::Error
...is that now you don't need to use
PartialEq
to understand what error was thrown.Error
has various types of downcasts and type checks if you do need to retrieve the concreteError
type out of your trait object.for number in numbers { for check in &mut checks { if let Some(error) = check.check_number(number) { println!("{}", error); if let Some(s_err)= error.downcast_ref::<EvenError>() { println!("custom logic for EvenErr: {} - {}", s_err.number, s_err) } } } }
full example on the playground