DEV Community

artech-git
artech-git

Posted on • Updated on

Creating a heterogenous type for holding several objects of different types in RUST

Type available within Rust supports composition of homogenous types, these are availabe in types such as

Vec<T>;  // an vector
[T;N];   // an array
();      // tuple 
Enter fullscreen mode Exit fullscreen mode

which can be used depending on various scenarios required ,
though you can use it as a heterogenous type but it is naturally constrained by the Sized trait which is applied to it during compile time which constrain us to further use it dynamically.


The key thing to note here is we can segregate our types based upon some common relation we can derive from them this will be the key idea to follow along towards achieving so

but when it comes to supporting the heterogenous types in rust things get little bit tricky here, to compose a type which can accommodate the objects from several types available using dyn trait which points the object in vtable which is behind a pointer
you can read here,

we want our type to be resizable during runtime therefore I will choose to go with Vec<T> type which can hold our objects

since whichever types applies this trait can directly reference a object using two techniques

1) using a reference to the object

   Vec<&dyn yourTrait>;
Enter fullscreen mode Exit fullscreen mode

2) placing the object behind a Box smart pointer

  Vec<Box<dyn youTrait>>;
Enter fullscreen mode Exit fullscreen mode

let's say the name of your trait is Foo and it declares one method to be implement

trait Foo {
  fn call(&self);
}
Enter fullscreen mode Exit fullscreen mode

now we will create some random types which will implement this
are as follows
(for the sake of simplicity I have defined a tuple struct here but your type can be much more complicated than this ... as long as you implement the trait you should be fine)

#[derive(Debug)]
struct A(i32); 
impl Foo for A {
    fn call(&self) {
        println!("type A: {:?}", self.0);
    }
}

#[derive(Debug)]
struct B(i32); 
impl Foo for B {
    fn call(&self) {
        println!("type B: {:?}", self.0);
    }
}

#[derive(Debug)]
struct C(i32); 
impl Foo for C {
    fn call(&self) {
        println!("type C: {:?}", self.0);
    }
}
Enter fullscreen mode Exit fullscreen mode

here you will observe one strange thing which is all the types implement the same exact definition of the trait methods but this can be different depending upon the operations you wanna do with that type.


now lets begin declaring a type which can simply hold them
according the first method

*1) using the direct reference to the object *

one important thing to note here is, the reference you gonna provide for your types will either be mutable or immutable only !

which simply mean, it applies to all the trait objects uniformly and u can't have some trait objects passed on as mutably and others as immutably.

fn main() {
     //some random trait objects declared
    let a = &A(43); 
    let b = &B(73); 
    let c = &C(95); 

     // now we will compose our homogenous type based on the previously types
    let objs: Vec<&dyn Foo> = vec![a, b, c];

    for I in objs.iter() {
       I.call(); 
    }
}
Enter fullscreen mode Exit fullscreen mode

since our objects a, b, c holds a reference to the types A, B, C respectively we can pass them directly here to construct our homogenous type out of them
output :

type A: 43
type B: 73
type C: 95
Enter fullscreen mode Exit fullscreen mode

2) Using Box smart pointer

now we will try out another interesting method, where we will place our objects behind a Box smart pointer in this case you don't have to worry about initializing your traits objects during compile itself, instead you will place them behind a pointer pointing to the memory location on heap
since in this technique the rules of borrow doesn't apply there it is quite convinence to work with
also the plus side is our objects can be init. during runtime which is a added advantage



fn main() {

    //here we place our objects behind a box pointer 
    let a = Box::new(A(43)); 
    let b = Box::new(B(73)); 
    let c = Box::new(C(95)); 

    //our type hold a boxed trait object as type to accept
    let mut objs: Vec<Box<dyn Foo>> = vec![a,b,c] ;

    for I in objs.iter() {
        I.call();
    }

}
Enter fullscreen mode Exit fullscreen mode

here the output will be exactly same as the previous one but with more features of box pointer:

type A: 43
type B: 73
type C: 95
Enter fullscreen mode Exit fullscreen mode

there you .. go congratulations you successfully learned how to create a new type >>

Thank you 😊 so much for reading till here 🥂🎉🎊!

if you have any comments, suggestion's, doubt or advice feel free to post in comment section

Top comments (0)