---
title: "Heterogeneous collections in Rust"
description: "Efficiently manage heterogenous collections in Rust using enums, traits, or the `Any` type.  Learn the tradeoffs of each approach, from stack vs. heap allocation to accessing specific methods, and discover how to reduce boilerplate with procedural macros for optimal code structure.\n"
slug: "Heterogeneous-collections-in-Rust"
created: 2023-10-03T00:00:00Z
updated: 2023-10-03T00:00:00Z
tags:
  - "rust"
ai_assisted: false
---

In some occasions, when programming software, developers run into the need of heterogenous
collections - that is, a collection that can store objects of different types. In Rust, there are
different ways a developer can achieve that, with different tradeoffs. This article will look into
a few different ways to achieve this.

## Using Enums
Rust [enums][1] are a great way to achieve this. Provided that all implementations of the objects
to be store are known at development time, developers can create an enum that wraps each possible
type, then create a collection for those enums.

Then, to access the methods and attributes of the inner class, a [match expression][2] can be used
to retrieve the inner object.

```rust
enum ComponentType {
    FirstComponent(MyFirstComponent),
    SecondComponent(MySecondComponent),
}

struct MyFirstComponent {
}

impl MyFirstComponent {
    fn do_first_component_thing(&self) {
        println!("First Component");
    }
}

struct MySecondComponent {
}

impl MySecondComponent {
    fn do_second_component_thing(&self) {
        println!("Second Component");
    }
}

fn main() {
    // Create a collection of enums;
    let mut components: Vec<ComponentType> = Vec::new();

    // Add the enums to the collection, wrapping the target type.
    components.push(ComponentType::FirstComponent(MyFirstComponent {}));
    components.push(ComponentType::SecondComponent(MySecondComponent {}));

    // Use match expressions to retrieve the object from the enum and access methods and attributes.
    if let ComponentType::FirstComponent(component) = &components[0] {
        component.do_first_component_thing();
    }
}
```

An advantage of this method is that the implementation is quite simple and idiomatic and, when used
inside an Array, for instance, it will allocate all objects on the stack (the `Vector` used in this
example will allocate on the heap, though).

On the other hand, a challenge with this approach is that those component types need to be known
when writing the code. Think about a library that needs to store objects from different types, but
those are only known by the user of that library.

## Using Traits

An alternative is using [traits][3] as alternate solution, where:
 - the common methods for Components are defined in the `Component` trait.
 - each relevant component struct implements the `Component` trait.
 - since the size of the objects being added to the collection are not know, the objects need to be wrapped in a [Box][4].

```rust

// Declare a trait with common behaviour.
trait Component {
    fn do_component_thing(&self);
}

struct MyFirstComponent {}

// Implement the trait for each type.
impl Component for MyFirstComponent {
    fn do_component_thing(&self) {
        println!("First Component");
    }
}

struct MySecondComponent {}

impl Component for MySecondComponent {
    fn do_component_thing(&self) {
        println!("Second Component");
    }
}

fn main() {
    let mut components: Vec<Box<dyn Component>> = Vec::new();
    components.push(Box::new(MyFirstComponent { }));
    components.push(Box::new(MySecondComponent { }));

    components[0].do_component_thing();
    components[1].do_component_thing();
}
```

This approach works well when it's only necessary to access the common method in all traits. A
disadvantage of this approach is that elements will always be allocated on the heap, and another
disadvantage is that it's only possible to access common methods.

## Using Any

The Rust documentation describes the [Any][5] type as *A trait to emulate dynamic typing.*. It
provides a `downcast` method, which allows typecasting to different types.

```rust
use std::any::Any;

struct MyFirstComponent {
}

impl MyFirstComponent {
    fn do_first_component_thing(&self) {
        println!("First Component");
    }
}

struct MySecondComponent {
}

impl MySecondComponent {
    fn do_second_component_thing(&self) {
        println!("Second Component");
    }
}

fn main() {
    let mut components: Vec<Box<dyn Any>> = Vec::new();
    components.push(Box::new(MyFirstComponent {}));
    components.push(Box::new(MySecondComponent {}));

    if let Some(component) =
            components[0].downcast_ref::<MyFirstComponent>() {
        component.do_first_component_thing();
    }

    if let Some(component) =
            components[1].downcast_ref::<MySecondComponent>() {
        component.do_second_component_thing();
    }
}
```

While this will still always allocate objects on the heap, it's now possible to have different
component types inside the data structure, cast them to original types and access component
specific attributes and methods.

There's one small issue, though - there is no bound to which types can be added to the structure
and the line below wokis just fine:

```rust
components.push(Box::new("I shouldn't be here").to_string());
```

## Mixing Any and Traits

`Any` can be used along with `Traits` to create bounds for the object. The trick is to add a method
to the trait that converts the object to `Any`, which will then be downcasted to other objects.
Each structure will then have to implement the trait, and the conversion method:

```rust
use std::any::Any;

trait Component {
    fn as_any(&self) -> &dyn Any;
}

struct MyFirstComponent {
}

impl MyFirstComponent {
    fn do_first_component_thing(&self) {
        println!("First Component");
    }
}

impl Component for MyFirstComponent {
    fn as_any(&self) -> &dyn Any {
        self
    }
}

struct MySecondComponent {
}

impl MySecondComponent {
    fn do_second_component_thing(&self) {
        println!("Second Component");
    }
}

impl Component for MySecondComponent {
    fn as_any(&self) -> &dyn Any {
        self
    }
}

fn main() {
    let mut components: Vec<Box<dyn Component>> = Vec::new();
    components.push(Box::new(MyFirstComponent {}));
    components.push(Box::new(MySecondComponent {}));

    if let Some(component) =
            components[0].as_any().downcast_ref::<MyFirstComponent>() {
        component.do_first_component_thing();
    }

    if let Some(component) =
            components[1].as_any().downcast_ref::<MySecondComponent>() {
        component.do_second_component_thing();
    }
}
```

While, again, this will still allocate objects on the heap, object specific methods and attributes
can be used with a downcast, and the collection is bound to objects that implement that trait. One
big downside is having to implement the trait for each object, which is just boilerplate.

### Using proc-macro-derive to avoid boilerplate

A solution to the boilerplate using using a [procedural macro][6] to implement the boiler plate:

```rust

// A derive macro needs to live in its own crate.
#[proc_macro_derive(Component)]
pub fn component_macro_derive(input: TokenStream) -> TokenStream {
    let ast: DeriveInput = syn::parse(input).unwrap();
    let name = &ast.ident;
    let gen = quote! {
        impl Component for #name {
            fn as_any(&self) -> &dyn Any {
                self
            }
        }
    };
    gen.into()
}

// The component still lives in the project file.
#[derive(Component)]
struct MyFirstComponent {
}

impl MyFirstComponent {
    fn do_first_component_thing(&self) {
        println!("First Component");
    }
}
```

# Conclusion

There are different ways to implement heterogenous collectionos in Rust. While the enums approach
seems to be considered the most idiomatic, it's not always possible to be used. In those cases,
different approaches are available, with their own tradeoffs.

[1]: https://doc.rust-lang.org/reference/items/enumerations.html
[2]: https://doc.rust-lang.org/reference/expressions/match-expr.html
[3]: https://doc.rust-lang.org/reference/items/traits.html
[4]: https://doc.rust-lang.org/std/boxed/struct.Box.html
[5]: https://doc.rust-lang.org/std/any/trait.Any.html
[6]: https://doc.rust-lang.org/reference/procedural-macros.html#derive-macros