How to communicate between trait objects of same type?

Hi everyone!

I have an array of trait objects Vec<Box<dyn SomeTrait>> where each concrete struct has one custom data field (e.g. String, i32, etc.). For any two objects of the same type (i.e. the data field is the same type), I would like to be able to set the data field by looking at each other.

How can I write code to achieve this?

Below is an example of what I'm trying to do:

trait SomeTrait {
    /// Display data member
    fn print(&self);
    /// What I would like to do
    fn set_data_if_same_type(&mut self, other: Box<dyn SomeTrait>);
}

struct A {
    data: String,
}

impl SomeTrait for A {
    fn set_data_if_same_type(&mut self, other: Box<dyn SomeTrait>) {
        // First check if the trait object is of the same type
        // Then set self's data from `other` if the type matches
    }
    fn print(&self) {
        println!("Text = {}", self.data);
    }
}

struct B {
    data: i32,
}

impl SomeTrait for B {
    fn set_data_if_same_type(&mut self, other: Box<dyn SomeTrait>) {
        // First check if the trait object is of the same type
        // Then set self's data from `other` if the type matches
    }
    fn print(&self) {
        println!("Number = {}", self.data);
    }
}

fn main() {
    let mut arr: Vec<Box<dyn SomeTrait>> = vec![
        Box::new(A {
            data: "Hello".to_owned(),
        }),
        Box::new(A {
            data: "World".to_owned(),
        }),
        Box::new(B { data: 3 }),
    ];

    arr.iter().for_each(|s| s.print());
    // Output:
    // Text = Hello
    // Text = World
    // Number = 3

    // If an element is of type `A`, override it's data field with given instance's data field
    arr.iter_mut().for_each(|s| {
        s.set_data_if_same_type(Box::new(A {
            data: "!".to_owned(),
        }))
    });
    // Desired Output:
    // Text = "!"
    // Text = "!"
    // Number = 3
}

You cannot do this directly with the type-system. You have to introduce an ad-hoc fn get_kind(&self) -> Kind method to your trait definition. Different implementors of this trait will return different values of the enum Kind.
But then again, if you can do this, you can avoid Box<dyn SomeTrait> entirely and make all the alternatives branches of an enum.

1 Like

You could use Any to achieve this.

use std::any::Any;

trait SomeTrait {
    /// Display data member
    fn print(&self);
    /// What I would like to do
    fn set_data_if_same_type(&mut self, other: &dyn SomeTrait);
    /// Necessary for implementing set_data_if_same_type:
    fn get_data(&self) -> &dyn Any;
}

struct A {
    data: String,
}

impl SomeTrait for A {
    fn set_data_if_same_type(&mut self, other: &dyn SomeTrait) {
        // First check if the trait object is of the same type
        if let Some(data) = other.get_data().downcast_ref::<String>() {
            // Then set self's data from `other` if the type matches
            self.data.clone_from(data);
        }
    }
    fn print(&self) {
        println!("Text = {}", self.data);
    }
    fn get_data(&self) -> &dyn Any {
        &self.data
    }
}

struct B {
    data: i32,
}

impl SomeTrait for B {
    fn set_data_if_same_type(&mut self, other: &dyn SomeTrait) {
        // First check if the trait object is of the same type
        if let Some(data) = other.get_data().downcast_ref::<i32>() {
            // Then set self's data from `other` if the type matches
            self.data = *data;
        }
    }
    fn print(&self) {
        println!("Number = {}", self.data);
    }
    fn get_data(&self) -> &dyn Any {
        &self.data
    }
}

fn main() {
    let mut arr: Vec<Box<dyn SomeTrait>> = vec![
        Box::new(A {
            data: "Hello".to_owned(),
        }),
        Box::new(A {
            data: "World".to_owned(),
        }),
        Box::new(B { data: 3 }),
    ];

    arr.iter().for_each(|s| s.print());
    // Output:
    // Text = Hello
    // Text = World
    // Number = 3

    // If an element is of type `A`, override it's data field with given instance's data field
    let exclamation_mark = A {
        data: "!".to_owned(),
    };
    arr.iter_mut()
        .for_each(|s| s.set_data_if_same_type(&exclamation_mark));

    arr.iter().for_each(|s| s.print());
    // Desired Output:
    // Text = "!"
    // Text = "!"
    // Number = 3
}

As a subsequent step, we could discuss how to remove the boilerplate (code repetition) if you need to implement this trait a lot.

3 Likes

I do not know your use case, but I would try to use an enum instead of the trait object. This is in my experience the better tradeoff.

1 Like

Thank you @steffahn for the reply!

I didn't know about Any trait, so this was a good learning for me in its usecase :slight_smile:

My usecase only involves a handful of concrete types so I am able to work with your solution, but I would be very interested to learn how you could reduce the boilerplate code if you don't mind sharing a bit more!

Thank you!

Thank you @RedDocMD for the quick reply! With your suggestion I was able to identify each concrete type (A and B) with an enum at runtime, but I was not able to figure out the underlying data type within A and B to override. I guess this can be trivially solved by defining an enum type to hold data field:

enum Data{
   String(String),
   Number(i32),
}

Your suggestion of using enum instead of trait object is normally what I prefer as well. For this particular case, I am investigating the trait object approach because I may want to expose this trait for users to implement on their custom types. My thought here was that if I used trait object, I can make it extensible from user-side without affecting my code as would happen with the use of enum.

Thank you for taking the time to provide your knowledge and helping me figure it out! :slight_smile:

Right... I suppose there are multiple approaches; one straightforward way I can think of involves a blanket impl based on another trait:

use std::any::Any;

trait SomeTraitImpl {
    type Data: Any + Clone;
    fn get_data(&self) -> &Self::Data;
    fn get_data_mut(&mut self) -> &mut Self::Data;
    fn print(this: &Self); // not `&self` to avoid conflicting method names / ambiguity
}

trait SomeTrait {
    /// Display data member
    fn print(&self);
    /// What I would like to do
    fn set_data_if_same_type(&mut self, other: &dyn SomeTrait);
    /// Necessary for implementing set_data_if_same_type:
    fn get_data_any(&self) -> &dyn Any;
}

impl<T: SomeTraitImpl> SomeTrait for T {
    fn set_data_if_same_type(&mut self, other: &dyn SomeTrait) {
        // First check if the trait object is of the same type
        if let Some(data) = other.get_data_any().downcast_ref::<T::Data>() {
            // Then set self's data from `other` if the type matches
            self.get_data_mut().clone_from(data);
        }
    }
    fn print(&self) {
        SomeTraitImpl::print(self)
    }
    fn get_data_any(&self) -> &dyn Any {
        self.get_data()
    }
}

struct A {
    data: String,
}

impl SomeTraitImpl for A {
    type Data = String;
    fn get_data(&self) -> &Self::Data {
        &self.data
    }
    fn get_data_mut(&mut self) -> &mut Self::Data {
        &mut self.data
    }
    fn print(this: &Self) {
        println!("Text = {}", this.data);
    }
}

struct B {
    data: i32,
}

impl SomeTraitImpl for B {
    type Data = i32;
    fn get_data(&self) -> &Self::Data {
        &self.data
    }
    fn get_data_mut(&mut self) -> &mut Self::Data {
        &mut self.data
    }
    fn print(this: &Self) {
        println!("Number = {}", this.data);
    }
}

fn main() {
    let mut arr: Vec<Box<dyn SomeTrait>> = vec![
        Box::new(A {
            data: "Hello".to_owned(),
        }),
        Box::new(A {
            data: "World".to_owned(),
        }),
        Box::new(B { data: 3 }),
    ];

    arr.iter().for_each(|s| s.print());
    // Output:
    // Text = Hello
    // Text = World
    // Number = 3

    // If an element is of type `A`, override it's data field with given instance's data field
    let exclamation_mark = A {
        data: "!".to_owned(),
    };
    arr.iter_mut()
        .for_each(|s| s.set_data_if_same_type(&exclamation_mark));

    arr.iter().for_each(|s| s.print());
    // Desired Output:
    // Text = "!"
    // Text = "!"
    // Number = 3
}
2 Likes

I see! So if my understanding is correct, the blanket impl (impl<T: SomeTraitImpl> SomeTrait for T) would contain any business logic that I would prefer not to duplicate, and the second trait (SomeTraitImpl) would simply contain get/get_mut methods for accessing the underlying data, thereby reducing the overall code duplication.

Based on the amount of business logic I want to put into SomeTrait I would evaluate whether to introduce the second trait (SomeTraitImpl), but it's good to know how to introduce the blanket impl in this context. Thanks for the help, I'm learning new things here :slight_smile:

1 Like

Yup, that’s exactly the tradeoff.

For extreme cases (i.e. lots of implementations, all have the same field names anyways, etc…), you could even resort to macros in order to avoid the duplication of defining those get_data/get_data_mut methods.

1 Like