axolotlKing0722
axolotlKing0722

Reputation: 262

How to use the `dyn` keyword for multiple traits in a vector when one trait has to be `Sized`?

My problem is: I have a function do_something, which expects a vector of objects. The objects can be of different types, but they all have to implement the traits A and B.

I declared examples of A and B like this:

trait A: Debug {
    fn say_hello_a(&self) {
        println!("hello a {:?}", self);
    }
}

trait B: Debug {
    fn say_hello_b(&self) {
        println!("hello b {:?}", self);
    }
}

Now, what I want is something like that:

fn do_something(data: Vec<&dyn A + B>) {
    // ...
}

However, as the dyn A + B syntax doesn't exist in rust, I have to use a wrapper type like this:

trait Wrapper: A + B {}

fn do_something(data: Vec<&dyn Wrapper>) {
    // ...
}

That seems to work. But the problem is now, in my specific problem, one of the traits, let's say A, has to be Sized, like this:

trait A: Debug + Sized {
    fn say_hello_a(&self) {
        println!("hello a {:?}", self);
    }
}

trait B: Debug {
    fn say_hello_b(&self) {
        println!("hello b {:?}", self);
    }
}

And now, the compiler throws an error at the point where Vec<&dyn Wrapper> is the type of an argument in the function:

the trait `Wrapper` cannot be made into an object

and says it is becuase A needs the object to be Sized. But even if I add + Sized to the definition of Wrapper, it doesn't work.

So what should I do instead?

For anyone wondering: My specific problem is that I'm working on a tauri app. The function that gets the Vec as an argument is a command which should be called from the frontend. Thus, the Vec items need to implement serde::Deserialize. But I then want to store those values in an SQLite database using rusqlite, so the Vec items should also implement rusqlite::ToSql. And I need a way to tell my compiler the Vec items should implemenet both of these traits.

Upvotes: 0

Views: 111

Answers (2)

axolotlKing0722
axolotlKing0722

Reputation: 262

Ok, as it seems like you're not able to use dyn with Sized traits / objects. However I find a solution that works for my original problem, maybe it also works for other usecases.

The original problem was that my function should accept arguments of every type that can be inserted in an SQL command, so everything that impements ToSql. However, all arguments of a tauri command must be Sized so I can't use dyn for these arguments. So it might not be possible to accept every type of argument that implements ToSql, but as I'm just interested in some of those types, like String or u64 (or other integer types), I decided to create an Enum which holds the various types an argument can have:

pub enum SqlArg {
    TypeStr(String),
    TypeInt(u64),
    TypeBool(bool),
    TypeNullableStr(Option<String>),
    // whatever types I need ...
}

Then, I created a function that converts a collection of SqlArg values to a collection of &dyn ToSql values:

trait SqlArgCollection {
    fn as_params(&self) -> Vec<&dyn ToSql>;
}

impl SqlArgCollection for Vec<SqlArg> {
    fn as_params(&self) -> Vec<&dyn ToSql> {
        self.iter().map(|element| match element {
            SqlArg::TypeInt(v) => v as &dyn ToSql,
            SqlArg::TypeStr(v) => v as &dyn ToSql,
            SqlArg::TypeBool(v) => v as &dyn ToSql,
            SqlArg::TypeNullableStr(v) => v as &dyn ToSql,
            // and so on...
        }).collect::<Vec<&dyn ToSql>>()
    }
}

That's a bit boilerplate because you need a match arm for every type, but that seemed to be the only solution.

However, now I can convert a Vec of SqlArg values to a Vec of &dyn ToSql values. For rusqlite to accept this collection, I need to convert it to a slice (but this has to be outside the as_params function, because otherwise you'd return a reference to a temporary value. There might be a workaround by using Box, but I'm fine with it how it is.).

#[tauri::command]
fn my_tauri_command(args: Vec<SqlArg>) {
    rusqlite::Connection::open("my_database.db").unwrap()
    .execute(
        "insert into SomeTable values (?1, ?2, ?3);",
        args.as_params().as_slice()
    ).unwrap();
}

And lastly, I made the SqlArg enum untagged, so it can be build from a string or int or whatever type, and the enum variant TypeStr, TypeInt etc. doesn't have to be mentioned.

#[derive(serde::Deserialize)]
#[serde(untagged)]
pub enum SqlArg {
    // ...
}

Upvotes: 0

Colin D Bennett
Colin D Bennett

Reputation: 12094

In order to refer to a value through dynamic dispatch (via dyn Trait syntax), the trait must be object-safe (see dyn keyword documentation).

One requirement of object-safety is that a trait must not require implementing types to be Sized (see object safety in traits documentation).

One way to understand why this must be true is that when you define a trait, there are potentially unlimited number of types implementing that trait, and each one is generally going to have a different size of its representation, and in fact you can't generally know all the implementing types because some other module might define another implementing type with a different size.

To solve your particular problem, I'd suggest making the Vec hold onto a Box<dyn Wrapper> and then it won't require Wrapper to be Sized.

Upvotes: 2

Related Questions