Reputation: 337
I'm new to rust and having some difficulty trying to use a Trait as an object with the end goal of storing a collections of structs that implement the Trait.
I'm trying to implement the following but I'm not sure how to resolve the compiler errors. The underlying issue seems to be my use of Self
as a parameter of a function defined by a trait:
trait MyTrait {
fn add(&mut self, data: Self) /*where Self: Sized*/;
}
struct Foo {
x: i32,
}
impl MyTrait for Foo {
fn add(&mut self, data: Self) {
self.x += data.x;
}
}
struct Bar {
s: String,
}
impl MyTrait for Bar {
fn add(&mut self, data: Self) {
self.s.push_str(&data.s);
}
}
fn main() {
let mut foo = Foo { x: 42 };
let mut bar = Bar { s: "hello".to_string() };
foo.add(Foo { x: 1 });
bar.add(Bar { s: " there!".to_string() });
let v: Vec<Box<dyn MyTrait>> = vec![Box::new(foo), Box::new(bar)];
//v[0].add(Foo { x: 2 });
//v[1].add(Bar { s: " general kenobi".to_string() });
assert_eq!(45, foo.x);
assert_eq!("hello there! general kenobi", bar.s);
}
The compiler gives me errors like:
rror[E0038]: the trait `MyTrait` cannot be made into an object
--> src/main.rs:33:12
|
30 | let v: Vec<Box<dyn MyTrait>> = vec![Box::new(foo), Box::new(bar)];
| ^^^^^^^^^^^^^^^^^^^^^ `MyTrait` 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/main.rs:2:29
|
1 | trait MyTrait {
| ------- this trait cannot be made into an object...
2 | fn add(&mut self, data: Self);
| ^^^^ ...because method `add` references the `Self` type in this parameter
= help: consider moving `add` to another trait
The compiler suggests moving my function to a new Trait... but I'm not sure how that helps me.
I really want to be able to dispatch these from the vector as shown in the commented out code, but that seems to make the situation worse.
I found How to use a trait object that contains a method that returns a reference to Self? but I'm not sure the solution is the same for using Self
as a parameter instead of a return type.
What am I doing wrong?
EDIT
It seems despite the trait itself being object-safe, the underlying issue is my add()
method cannot be dispatched on a trait object as described in the docs. This is because its possible that Self may not be the same concrete type as self.
My goal is to store a collection of some base type where concrete instances know how to mutate themselves and operate over other instances of themselves. The types of these objects is not necessarily known at compile time. I've implemented such things in many other languages (sometimes with the help of a cast), how would I translate such a design to Rust?
Upvotes: 0
Views: 1180
Reputation: 18619
What you're trying to do, doesn't really make sense. Hear me out!
As far as types are concerned, all instances of a type must behave identically (that's how we know how to handle them). This applies to trait objects too: all instances of the dyn Trait
type has to behave identically.
Self
is a placeholder: though it's spelled the same, it actually stands for a different type in each impl
.
So what you're trying to do is create a type, dyn MyTrait
, whose add
method takes a value of type... um... sometimes Foo
, sometimes Bar
? That doesn't work!
That's why the compiler is yelling at you.
This model can not work the way you described. The solution depends on what your actual requirements are:
Disable the add
method on the trait objects of your trait
You can do this by adding a where Self: Sized
bound to the add
method. But this means that you can only call the add method before casting to a trait object (i.e. before you put them into the Vec
).
Make the add
function fallible, and let it decide whether it can accept a particular value
Use enum
s
If you know all types that implement your trait ahead of time, you can swap out your trait for an enum
. The enum
may contain the different fields of the different types, or, if you also want to use the types separately, you can just wrap them in the enum.
It would look like this:
#[derive(Debug)]
struct DifferentTypesError;
enum MyEnum {
Foo(Foo),
Bar(Bar)
}
struct Foo {
x: i32,
}
struct Bar {
s: String,
}
impl MyEnum {
fn add(&mut self, data: Self) -> Result<(), DifferentTypesError> {
use MyEnum::*;
match (self, data) {
(Foo(self_), Foo(data)) => {
self_.x += data.x;
Ok(())
},
(Bar(self_), Bar(data)) => {
self_.s.push_str(&data.s);
Ok(())
},
(_, _) => Err(DifferentTypesError)
}
}
}
impl From<Foo> for MyEnum {
fn from(v: Foo) -> Self {
Self::Foo(v)
}
}
impl From<Bar> for MyEnum {
fn from(v: Bar) -> Self {
Self::Bar(v)
}
}
fn main() {
let mut foo = MyEnum::from(Foo { x: 42 });
let mut bar = MyEnum::from(Bar { s: "hello".to_string() });
foo.add(Foo { x: 1 }.into()).unwrap();
bar.add(Bar { s: " there".to_string() }.into()).unwrap();
let mut v: Vec<MyEnum> = vec![foo, bar];
v[0].add(Foo { x: 5 }.into()).unwrap();
v[1].add(Bar { s: " again".to_string() }.into()).unwrap();
}
Use dynamic typing with the Any
trait
The Any
trait can be used to attempt to downcast to a specific type. Here's how that would work:
use core::any::Any;
#[derive(Debug)]
struct DifferentTypesError;
trait MyTrait {
fn add(&mut self, data: Box<dyn Any>) -> Result<(), DifferentTypesError>;
}
struct Foo {
x: i32,
}
impl MyTrait for Foo {
fn add(&mut self, data: Box<dyn Any>) -> Result<(), DifferentTypesError> {
match data.downcast::<Self>() {
Ok(data) => {
self.x += data.x;
Ok(())
},
Err(_) => Err(DifferentTypesError)
}
}
}
struct Bar {
s: String,
}
impl MyTrait for Bar {
fn add(&mut self, data: Box<dyn Any>) -> Result<(), DifferentTypesError> {
match data.downcast::<Self>() {
Ok(data) => {
self.s.push_str(&data.s);
Ok(())
},
Err(_) => Err(DifferentTypesError)
}
}
}
fn main() {
let mut foo = Foo { x: 42 };
let mut bar = Bar { s: "hello".to_string() };
foo.add(Box::new(Foo { x: 1 }) as Box<dyn Any>).unwrap();
bar.add(Box::new(Bar { s: " there".to_string() }) as Box<dyn Any>).unwrap();
let mut v: Vec<Box<dyn MyTrait>> = vec![Box::new(foo), Box::new(bar)];
v[0].add(Box::new(Foo { x: 5 }) as Box<dyn Any>).unwrap();
v[1].add(Box::new(Bar { s: " again".to_string() }) as Box<dyn Any>).unwrap();
}
Do it in a Rustier way
Since I don't know what exactly are you trying to implement, I can't help with that, but I think it's likely that there's a more idiomatic way to solve your problem.
Upvotes: 4
Reputation: 13750
The issue here is that Self
makes the trait “object unsafe”. In short, the compiler correctly picks up on the fact that it wouldn't be able to check a call like v[0].add(data)
because it can't tell if data: Self
, i.e., that data
has the same type as v[0]
, because it doesn't know the type of v[0]
anymore (you erased it when you created the trait object).
Moving add
to a separate trait would allow you to create a dyn MyTrait
, but of course at the expense of MyTrait
no longer having an add
method.
Upvotes: 0