Papulatus
Papulatus

Reputation: 687

Type handling in the trait object

I'm new to rust and I recently ran into a problem with trait

I have a trait that is used as the source of a message and is stored in a structure as a Box trait object. I simplified my logic and the code looks something like this.

#[derive(Debug)]
enum Message {
    MessageTypeA(i32),
    MessageTypeB(f32),
}

enum Config {
    ConfigTypeA,
    ConfigTypeB,
}

trait Source {
    fn next(&mut self) -> Message;
}

struct SourceA;

impl Source for SourceA {
    fn next(&mut self) -> Message {
        Message::MessageTypeA(1)
    }
}

struct SourceB;

impl Source for SourceB {
    fn next(&mut self) -> Message {
        Message::MessageTypeB(1.1)
    }
}

struct Test {
    source: Box<dyn Source>,
}

impl Test {
    fn new(config: Config) -> Self {
        Test {
            source: match config {
                Config::ConfigTypeA => Box::new(SourceA{}),
                Config::ConfigTypeB => Box::new(SourceB{}),
            }
        }
    }

    fn do_sth(&mut self) -> String {
        match self.source.next() {
            Message::MessageTypeA(a) => format!("a is {:?}", a),
            Message::MessageTypeB(b) => format!("b is {:?}", b),
        }
    }
    
    fn do_sth_else(&mut self, message: Message) -> String {
        match message {
            Message::MessageTypeA(a) => format!("a is {:?}", a),
            Message::MessageTypeB(b) => format!("b is {:?}", b),
        }
    }
}

Different types of Source return different types of Message, the Test structure needs to create the corresponding trait object according to config and call next() in the do_sth function.

So you can see two enum types Config and Message, which I feel is a strange usage, but I don't know what's strange about it.

I tried to use trait association type, but then I need to specify the association type when I declare the Test structure like source: Box<dyn Source<Item=xxxx>> but I don't actually know the exact type when creating the struct object.

Then I tried to use Generic type, but because of the need of the upper code, Test cannot use Generic.

So please help me, is there a more elegant or rustic solution to this situation?

Upvotes: 1

Views: 304

Answers (1)

cadolphs
cadolphs

Reputation: 9647

So what might be going on here is that you're mis-using the idea of trait objects. The whole idea of a trait object is that you want to write code that relies purely on its interface. As soon as you find yourself checking for the underlying type of a trait object, that should raise a red flag.

Of course in your example you're not using any sort of weird run-time type checking; instead, you're checking the type implicitly via the enums.

Still, you recognize this as problematic. First, it becomes very clumsy when you try to add another variant to your sources, because now you have to go and add that to your message and config enums as well. Second, you then have to add that handling logic to everywhere the trait object is used. And finally, the type system "lies" a bit. It seems to me that source A will only ever send messages of the first variant and source B will only ever send messages of the second variant, and that's how we're telling them apart.

So what's a way out here?

First, trait objects should be designed such that they can be used without having to know which implementation we're dealing with. Traits represent roles that structs can play, and code that uses trait objects says "I'm happy to work with anyone who can play that role".

If your code isn't happy to work with anyone who can play that trait's role, there's a flaw in the design.

It would be good to know how you are, in general, processing the messages returned by the sources.

For example, does it matter for the rest of your program that source A only ever returns integers and source B only ever returns floats? And if so, what about it is it that matters, and could that be abstracted behind a trait?

Upvotes: 0

Related Questions