3

I would like to create an array containing struct types (non-instanced), all implementing a same trait. I have tried to do this:

trait TraitA {
    fn new(number: i16) -> Self;
    fn get_name() -> &'static str;
}

struct StructA {
    bar: u8
}
struct StructB {
    foo: i16
}

impl TraitA for StructA {
    fn new(number: i16) -> Self {
        StructA { bar: number as u8 }
    }
    fn get_name() -> &'static str
    { "StructA" }
}
impl TraitA for StructB {
    fn new(number: i16) -> Self {
        StructB { foo: number }
    }
    fn get_name() -> &'static str
    { "StructB" }
}

fn main() {
    let struct_array = [StructA, StructB];

    for i in 0..struct_array.len() {
        println!("{}", struct_array[i]::get_name());
        struct_array[i]::new(i);
    }
}

The compiler expects an actual value, not a type.

In Python, I would do as such:

class ClassA:
    def __init__(self, number):
        self.bar = number
    def get_name():
        return "ClassA"
class ClassB:
    def __init__(self, number):
        self.foo = number
    def get_name():
        return "ClassB"

if __name__ == "__main__":
    class_array = [ClassA, ClassB]
    for i in range(2):    
        print(class_array[i].get_name())
        class_array[i](i)

How could I achieve this ?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
op325
  • 139
  • 1
  • 12
  • Not the type, but maybe you could store a funcion that returns a boxed trait object? – rodrigo Oct 13 '20 at 09:08
  • @rodrigo apologies for my ignorance. How would I achieve this ? – op325 Oct 13 '20 at 09:12
  • 1
    Something like this [playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=bfbc263b5baca243f14cd50ee50e793d)? – rodrigo Oct 13 '20 at 09:14
  • @rodrigo that is very interesting thank you ! I forgot to specify that there are multiple "static" functions in the Trait – op325 Oct 13 '20 at 09:19
  • Question modified. – op325 Oct 13 '20 at 09:23
  • 3
    Maybe you should consider not using static functions and make a `Factory` trait or something that implements all this statics as non-static methods. Then just keep a vector of `Box` and done. – rodrigo Oct 13 '20 at 09:23
  • @rodrigo I've thought about something like that but I was wondering if there was a "cleaner" way to do this. – op325 Oct 13 '20 at 09:25
  • 5
    @op325 What rodrigo suggests is very "clean". Types are not first-class objects in Rust – they only exist at compile time, and don't have any representation at runtime. If you want a runtime representation, you need to define builder types yourself. These types can be zero-sized if you don't need to store any information on the builders. – Sven Marnach Oct 13 '20 at 09:29
  • @rodrigo can you propose an answer where you would do this pls ? I've been struggling for a few hours – op325 Oct 13 '20 at 19:45
  • 1
    [How does Rust store types at runtime?](https://stackoverflow.com/q/58104462/155423) – Shepmaster Oct 14 '20 at 01:21

1 Answers1

5

In Python, classes are also objects and callables, and calling them you create a new object of that type.

In Rust, as SvenMarnach commented above, types are not objects; they only exist at compile time and have no representation in the running program.

To do what you want you have to create a type that imitates the Python class type. Some kind of factory pattern, that will naturally be a trait. I'm assuming that you want to do something with those TraitA objects, other than creating them, so let's add something useful and move the building stuff to another trait:

trait TraitA {
    fn do_something(&self);
}
trait FactoryA {
    fn new(&self, number: i16) -> Box<dyn TraitA>;
    fn get_name(&self) -> &'static str;
}

Note that I'm adding a &self argument to every function. That is necessary because we'll want to invoke dynamic dispatching later, and in Rust you cannot have dynamic dispatch without self. But this trait represents your Python classes, so this member functions are analogous to Python class methods.

Also the new function could return an associated type, but that will not play well with the dynamic dispatching either, so I'm returning a type erased Box<dyn TraitA>.

Now, implementing a couple of classes is quite boring:

struct StructA {
    bar: u8
}
struct StructB {
    foo: i16
}

impl TraitA for StructA {
    fn do_something(&self) {
        println!("I'm an A({})", self.bar);
    }
}
impl TraitA for StructB {
    fn do_something(&self) {
        println!("I'm a B({})", self.foo);
    }
}

Implementing the factories is more interesting:

struct BuilderA;

struct BuilderB;

impl FactoryA for BuilderA {
    fn new(&self, number: i16) -> Box<dyn TraitA> {
        Box::new(StructA { bar: number as u8 })
    }
    fn get_name(&self) -> &'static str
    { "StructA" }
}
impl FactoryA for BuilderB {
    fn new(&self, number: i16) -> Box<dyn TraitA> {
        Box::new(StructB { foo: number })
    }
    fn get_name(&self) -> &'static str
    { "StructB" }
}

We use zero sized types (ZST) for the builders, because we have nothing to store there.

The main function is also quite straightforward. I've switched your loop into a Iterator::enumerate just for fun:

fn main() {
    let struct_array: Vec<Box<dyn FactoryA>> = vec![
        Box::new(BuilderA),
        Box::new(BuilderB),
    ];

    for (i, b) in struct_array.iter().enumerate() {
        println!("{}", b.get_name());
        let a = b.new(i as i16);
        a.do_something();
    }
}

This works as expected and prints (playground):

StructA
I'm an A(0)
StructB
I'm a B(1)
Brian61354270
  • 8,690
  • 4
  • 21
  • 43
rodrigo
  • 94,151
  • 12
  • 143
  • 190