It's often helpful to use helper functions for parsing things. We can implement this function on the trait itself to keep the parsing function associated with the trait.
We'll have the function return Result<Box<dyn animal>, ()>
since it's possible for parsing to fail. (We'd probably want a proper error type instead of ()
in real code.)
trait animal{}
impl dyn animal {
fn try_parse(kind: &str, data: &str) -> Result<Box<dyn animal>, ()> {
match kind {
"Cat" => Ok(Box::new(Cat { name: data.into() })),
"Dog" => Ok(Box::new(Dog { age: data.parse().map_err(|_e| ())? })),
_ => Err(()),
}
}
}
Ok, so now we have a function that can be used to parse a single animal, and has a way to signal failure. We could now parse a comma-separated string building off of this function, again signaling errors if the string doesn't contain a comma:
impl dyn animal {
// try_parse()
fn try_parse_comma_separated(input: &str) -> Result<Box<dyn animal>, ()> {
let split = input.split(',');
let parts = (split.next(), split.next());
// parts is a tuple of two Option<&str>. We can only proceed if both
// are Some.
match parts {
(Some(kind), Some(data)) => Self::try_parse(kind, data),
_ => Err(()),
}
}
}
Now our main()
is trivial:
fn main() {
let mut zoo: Vec<Box<dyn animal>> = vec![];
let user_input = "Cat,Persik";
zoo.push(<dyn animal>::try_parse_comma_separated(user_input).unwrap());
}
Separating things out like this allows us to reuse these functions in other interesting ways. Let's say you wanted to parse a string like "Cat,Persik,Dog,5"
as two values. That can now be done by using iterators and mapping over our parse function:
fn main() {
let user_input = "Cat,Persik,Dog,5";
let zoo = user_input.split(',').collect::<Vec<_>>()
.chunks_exact(2) // Group the input into slices of 2 elements each
.map(|s| <dyn animal>::try_parse(s[0], s[1]).unwrap())
.collect::<Vec<_>>();
}
To answer your question about a better way to do this when managing many implementors of animal
, you could move the implementation-specific parsing logic into a similar function on each implementation instead, and call that functionality from <dyn animal>::try_parse()
. The parsing logic has to live somewhere.
Doesn't this way limit me in using only methods defined in animal Trait on items of zoo? If so, is there a way around this constraint?
Without downcasting, yes. Generally when you have a collection of polymorphic values like dyn animal
, you want to use them polymorphically -- invoking only methods defined on the animal
trait. Each implementation of the trait on a specific type can implement the trait's interface however it makes sense for that animal.
Downcasting is non-trivial, but with a helper trait it becomes a bit more palatable:
trait AsAny {
fn as_any(&self) -> &dyn Any;
}
impl<T: 'static + animal> AsAny for T {
fn as_any(&self) -> &dyn Any { self }
}
trait animal: AsAny { }
Now, given an animal: Box<dyn Animal>
you can use animal.as_any().downcast_ref::<Dog>()
for example, which gives you back an Option<&Dog>
. This will be None
if the boxed animal isn't a dog. Based on the zoo
in the last example (with a dog and a cat):
let dogs = zoo.iter()
// Filter down the zoo to just dogs (produces a sequence of &Dog)
.filter_map(|animal| animal.as_any().downcast_ref::<Dog>());
// We should only find one dog in the zoo.
assert_eq!(dogs.count(), 1);
But this should be an absolute last resort when using your animals polymorphically isn't an option.