Vec with different types

Out of curiosity, I was exploring the possibility of creating and manipulating a vec containing variables of different types. The only way I found was to generate a vec as follows:

    let arr: Vec<Box<dyn Any>> = vec![
        Box::new('a'),         // char
        Box::new(1),           // i32
        Box::new("hello"),      // &str
        Box::new(2.1f64),      // f64 ]

I could then use downcast_ref for further processing. Are there better ways?

Thx.

Using an enum would be more common:

enum MyEnum {
    C(char),
    I(i32),
    S(&'static str),
    F(f64),
}

fn main() {
    let arr: Vec<MyEnum> = vec![
        MyEnum::C('a'),         // char
        MyEnum::I(1),           // i32
        MyEnum::S("hello"),     // &str
        MyEnum::F(2.1f64),      // f64
    ];
}
6 Likes

Interesting... I had completely missed this path... Thanks.

Thinking about the 2 solutions... using enums is certainly clearer and I think it is much more efficient... but adding more elements to the array would imply modifying the enum if I'm not mistaken... at the level of type safety via enum we would go to compile time while using the solution I proposed we would be at runtime or am I wrong? Well, very interesting.

Only if the new elements have new types that you haven’t already used. Is this question just for curiosity/exploring, or do you have a use in mind for this heterogeneous Vec? If you do, there may be a better way to make it happen— But making a meaningful suggestion will require more context about your larger goal.

1 Like

My question is only for curiosity. I was analizing some C# code and i found an array of object that the programmer had mixed types into, so i ask myself if there was a way to do something similar in Rust.

This is one of the situations where what you should do in Rust depends on the specific requirements from the specific problem being solved. Box<dyn Any> (or rather, Box<dyn (some trait that has Any as a supertrait)>) is the closest to the freeform polymorphism that GC&OOP languages like C# give you by default, but it is still not the same thing, and it has costs (allocation, dynamic dispatch, lack of many operations) that should be avoided when not necessary.

And if the array of objects is actually more tuple-like — e.g. a key-value pair — then you should use a tuple or a tuple struct, instead.

8 Likes

At this point, a general question might be asked, regardless of the problem: is there an efficient way to index elements of different types in a sequence?

Additionally you may consider having different Vec for each type, as in Data Oriented Programming - Rust is not very suitable for OOP in a lot of ways.

A slice of enums is the most efficient since there is no dyn (vtable) indirection and you can stored owned values without boxing.

1 Like

It's important to note here that these costs are also there in C# or Java. @kpreid meant that Rust often offers better alternatives than what C# does by default. C# also hides most of the complexity and costs, while in Rust they are in your face all the time.

2 Likes

This comes with some caveats, most notably that all the enum variants should be roughly the same size, since you pay the cost of storing the largest variant for each element. dyn Trait has a bad reputation that it doesn't deserve.

1 Like

You're right -- it's true that a big variation in enum variant sizes would cause wasted space, but I think the best solution would be to box the large variants rather than using dyn, which would typically require boxing all values.

However, &dyn works without boxing if using temporary references is OK. And I agree that dyn has a lot of value in general.

Perhaps the key question is whether the OP wants to store owned values and wants to minimize allocations. I was replying to this, but we should ask what is meant by "efficient":

Efficient...could be simple, memory-efficient and fast... but I think we have to find a compromise between these elements.

The question is how these values should be used?
There should be some code that iterates over these values and does something.

You have two solutions here, and they are actually language-agnostic:

  1. Make this code aware of all variants. This is enum approach:
struct Bird;
struct Fish;

enum Boxes {
    WithBird(Bird),
    WithFish(Fish),
}


fn main() {
    let boxes = vec![Boxes::WithBird(Bird), Boxes::WithFish(Fish)];

    // I am the client of this code. I am aware of *all* the cases
    // Each time you add new type, all usages must be updated
    for b in boxes {
        match b {
            Boxes::WithBird(bird) => {}
            Boxes::WithFish(fish) => {}
        }
    }
}
  1. Make this code unaware of different types. Virtual dispatch can be used here.
struct Bird;
struct Fish;

trait Move {
    fn mov(&self);
}

impl Move for Bird {
    fn mov(&self) {
        println!("fly")
    }
}

impl Move for Fish {
    fn mov(&self) {
        println!("swim")
    }
}


fn main() {
    let boxes: Vec<Box<dyn Move>> = vec![Box::new(Bird), Box::new(Fish)];

    // I am a client. I have *no* idea what is there inside, I just call the virtual method
    // You can add lots of new "movers" without touch me
    for b in boxes {
        b.mov();
    }
}

What you came up with is an approach called dynamic dispatch. I.e. you're storing two pointers in the vec: one pointing to a function table containing functions specified by trait Any, and the other pointing to the object itself. A more cache-friendly approach is to use an enum, as Alice suggested. This stores the object in the array, but each element takes up enough space to store the biggest of the objects (biggest here is determined by std::mem::size_of::<T>()) plus the space for the variant tag plus padding. There's a third method, which is a hybrid of the first two: store an enum in the vec, but the enum's variants should contain an index into the vec associated with that variant. Which one is more efficient heavily depends on your specific use case.