Reputation: 3361
I am trying to implement a trait for a struct which in turn has functions that return traits. I want this, because I do not want to bind the uer to a specific data structure. However, trying to apply the compiler's correction suggestions, I fell deeper and deeper into a rabbit hole to no avail. Here's a minimal example of what I'm trying to do:
trait WordsFilter {
fn starting_with(&self, chr: char) -> dyn Iterator<Item = String>;
}
struct WordsContainer {
words: Vec<String>,
}
impl WordsFilter for WordsContainer {
fn starting_with(&self, chr: char) -> dyn Iterator<Item = String>
{
self.words.iter().filter(|word| word.starts_with("a"))
}
}
fn main() {}
Which results in:
error[E0277]: the size for values of type `(dyn Iterator<Item = String> + 'static)` cannot be known at compilation time
--> .\traits.rs:10:40
|
10 | fn starting_with(&self, chr: char) -> dyn Iterator<Item = String>
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `(dyn Iterator<Item = String> + 'static)`
= note: the return type of a function must have a statically known size
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
I tried to apply the compiler's correction's step by step but they were just getting more.
Upvotes: 1
Views: 860
Reputation: 73470
There are several things that need to be fixed to get this to work.
I'll first show the workng version then step through what had to change and why.
trait WordsFilter {
fn starting_with(&self, chr: char) -> Box<dyn Iterator<Item = String> + '_>;
}
struct WordsContainer {
words: Vec<String>,
}
impl WordsFilter for WordsContainer {
fn starting_with(&self, chr: char) -> Box<dyn Iterator<Item = String> + '_>
{
Box::new(self.words.iter().filter(|word| word.starts_with("a")).cloned())
}
}
fn main() {}
Boxing the iterator. Rust doesn't allow you to return naked dyn
types. The compiler error tells you to Box it. So I did.
Clone after filter. Vec<X>::iter()
returns an iterator with Item=&X
so in this example we get Item=&String
we want Item=String
so we need to clone.
Add a lifetime. The problem was that without the lifetime checks I could write this...
let words_container = WordsContainer{...};
let it = words_container.starting_with();
drop(words_container)
it.next()
but it.next()
is stepping through the internal vector that is inside words_container
- which no longer exists.
Adding the '_
lifetime on the trait is saying the iterator is only valid for as long as the underlying container is. So now the above code would generate a compile error.
Upvotes: 1
Reputation: 6061
TL;DR Use generics or associated types.
You cannot use bare dyn Trait
types. They are Unsized
which means that compiler cannot know their size at the compile time. Therefore you can only use them behind some reference (similarly to how you cannot use str
or [T]
and must use &str
and &[T]
). The easiest way is to use Box<dyn Trait>
, like PitaJ earlier suggested. This requires of course one heap allocation, but allows a very nice API.
In normal functions and methods you can however use impl Trait
trick to make compiler infer returned type. This is used like this
fn foo() -> impl Iterator<Item = ()> {
todo!()
}
Calling this function would return some concrete object that implements given trait. This however is currently not possible in Trait definitions. There is active development and hopefully in the future we will be able to use it. However now it's not the case. So following code will not compile!
// XXX: This doesn't currently work!
trait Foo {
fn foo() -> impl Iterator<Item = ()>;
}
There is luckily a simple solution that wouldn't require boxing trait objects. You can define an associated type and require each implementator to specify a concrete type. You can also require that this type must implement the Iterator
trait.
trait WordsFilter {
type Iter: Iterator<Item = String>;
fn starting_with(&self, chr: char) -> Self::Iter;
}
This would make each implementation specify one concrete type of returned iterator. If you would like to be able to return multiple different iterators for a single type you can use generics.
trait WordsFilter<I>
where
I: Iterator<Item = String>
{
fn starting_with(&self, chr: char) -> I;
}
Upvotes: 3